Skip to content

Commit

Permalink
docs: Implement Divider Component (momentum-design#978)
Browse files Browse the repository at this point in the history
* fix: Implement Divider Component

* Temporary build fix

* Temporary fix

* fix: ci errors, conventions

* updated the component

* fix: code refactoring

* fix: update styles

* Minor eslint fix

* Renamed button attribute

* updated styles with vertical token

* update text divider gradient styling and handle rtl

* no message

* Updated Storybook UI

* fix: address review comments

* fix: address A11y issues

* storybook cleanup

* removed aria-label and aria-expanded from mdc-divider

* Updated styles

* minor eslint fix

* Minor one

* update RTL visual of the divider

* fix: resolved review comments

* chore: removed redundant internal state

* fix: removed mutation observer

* fix: updated the styling without using mutation observer

---------

Co-authored-by: Supriya M <sansup49@gmail.com>
  • Loading branch information
2020Deeya and supminn authored Dec 17, 2024
1 parent bd7e0ef commit 3056af0
Show file tree
Hide file tree
Showing 8 changed files with 695 additions and 1 deletion.
17 changes: 16 additions & 1 deletion packages/components/config/storybook/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,19 @@ const disableControls = (keys: Array<string>) => {
return objectReturnValue;
};

export { disableControls };
/**
* To automatically hide controls in storybook
* @param keys - Array of keys to hide
* @returns Object which can be destructured in argTypes
*/
const hideControls = (keys: Array<string>) => {
const objectReturnValue: Record<string, any> = {};
keys.forEach((key) => {
objectReturnValue[key] = {
table: { disable: true }
};
});
return objectReturnValue;
};

export { disableControls, hideControls };
233 changes: 233 additions & 0 deletions packages/components/src/components/divider/divider.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { CSSResult, html, PropertyValueMap } from 'lit';
import { property } from 'lit/decorators.js';
import styles from './divider.styles';
import { Component } from '../../models';
import {
ARROW_ICONS,
BUTTON_TAG,
DEFAULTS,
DIRECTIONS,
DIVIDER_ORIENTATION,
DIVIDER_VARIANT,
TEXT_TAG,
} from './divider.constants';
import { Directions, DividerOrientation, DividerVariant } from './divider.types';

/**
* `mdc-divider` is a component that provides a line to separate and organize content.
* It can also include a button or text positioned centrally, allowing users to interact with the layout.
*
* **Divider Orientation:**
* - **Horizontal**: A thin, horizontal line.
* - **Vertical**: A thin, vertical line.
*
* **Divider Variants:**
* - **solid**: Solid line.
* - **gradient**: Gradient Line.
*
* **Divider Types:**
* - The type of divider is inferred based on the kind of slot present.
* - **Primary**: A simple horizontal or vertical divider.
* - **Text**: A horizontal divider with a text label in the center.
* - **Grabber Button**: A horizontal or vertical divider with a styled button in the center.
*
* **Accessibility:**
* - When the slot is replaced by an `mdc-button`:
* - `aria-label` should be passed to the `mdc-button`.
* - `aria-expanded` should be passed to the `mdc-button`.
*
* **Notes:**
* - If the slot is replaced by an invalid tag name or contains multiple elements,
* the divider defaults to the **Primary** type.
* - To override the styles of the divider, use the provided CSS custom properties.
*
* @tagname mdc-divider
*
* @cssproperty --mdc-divider-background-color - background color of the divider
* @cssproperty --mdc-divider-width - width of the divider
* @cssproperty --mdc-divider-horizontal-gradient - gradient of the horizontal divider
* @cssproperty --mdc-divider-vertical-gradient - gradient of the vertical divider
* @cssproperty --mdc-divider-text-size - font size of label in the text divider
* @cssproperty --mdc-divider-text-color - font color of label in the text divider
* @cssproperty --mdc-divider-text-margin - left and right margin of label in the text divider
* @cssproperty --mdc-divider-text-line-height - line height of label in the text divider
* @cssproperty --mdc-divider-grabber-button-border-radius - border radius of the grabber button
*/
class Divider extends Component {
/**
* Two orientations of divider
* - **horizontal**: A thin, horizontal line with 0.0625rem width.
* - **vertical**: A thin, vertical line with 0.0625rem width.
*
* Note: We do not support "Vertical Text Divider" as of now.
* @default horizontal
*/
@property({ type: String, reflect: true })
orientation: DividerOrientation = DEFAULTS.ORIENTATION;

/**
* Two variants of divider
* - **solid**: Solid line.
* - **gradient**: Gradient Line that fades on either sides of the divider.
* @default solid
*/
@property({ type: String, reflect: true })
variant: DividerVariant = DEFAULTS.VARIANT;

/**
* Direction of the arrow icon, if applicable.
* - **positive**
* - **negative**
*
* Note: Positive and Negative directions are defined based on Cartesian plane.
* @default 'negative'
*/
@property({ type: String, attribute: 'arrow-direction', reflect: true })
arrowDirection: string = DEFAULTS.ARROW_DIRECTION;

/**
* Position of the button, if applicable.
* - **positive**
* - **negative**
*
* Note: Positive and Negative directions are defined based on Cartesian plane.
* @default 'negative'
*/
@property({ type: String, attribute: 'button-position', reflect: true })
buttonPosition: string = DEFAULTS.BUTTON_DIRECTION;

/**
* Sets the variant attribute for the divider component.
* If the provided variant is not included in the DIVIDER_VARIANT,
* it defaults to the value specified in DEFAULTS.VARIANT.
*
* @param variant - The variant to set.
*/
private setVariant(variant: DividerVariant) {
this.setAttribute('variant', Object.values(DIVIDER_VARIANT).includes(variant) ? variant : DEFAULTS.VARIANT);
}

/**
* Sets the orientation attribute for the divider component.
* If the provided orientation is not included in the DIVIDER_ORIENTATION,
* it defaults to the value specified in DEFAULTS.ORIENTATION.
*
* @param orientation - The orientation to set.
*/
private setOrientation(orientation: DividerOrientation) {
this.setAttribute(
'orientation',
Object.values(DIVIDER_ORIENTATION).includes(orientation) ? orientation : DEFAULTS.ORIENTATION,
);
}

/**
* Sets the buttonPosition and arrowDirection attribute for the divider component.
* If the provided buttonPosition and arrowDirection are not included in the DIRECTIONS,
* it defaults to the value specified in DIRECTIONS based on the ORIENTATION.
*
* @param buttonPosition - The buttonPosition to set.
* @param arrowDirection - The arrowDirection to set.
*/
private ensureValidDirections() {
const defaultDirection = this.orientation === DIVIDER_ORIENTATION.HORIZONTAL
? DIRECTIONS.NEGATIVE
: DIRECTIONS.POSITIVE;

if (!Object.values(DIRECTIONS).includes(this.buttonPosition as Directions)) {
this.buttonPosition = defaultDirection;
}

if (!Object.values(DIRECTIONS).includes(this.arrowDirection as Directions)) {
this.arrowDirection = defaultDirection;
}
}

/**
* Configures the grabber button within the divider.
*
* - Sets the `prefix-icon` attribute for the grabber button based
* on the `arrow-direction` and `orientation` properties.
*
* This method updates the DOM element dynamically if a grabber button is present.
*/
private setGrabberButton(): void {
const buttonElement = this.querySelector('mdc-button');
if (!buttonElement) return;

this.ensureValidDirections();
const iconType = this.getArrowIcon();
buttonElement.setAttribute('variant', 'secondary');
buttonElement.setAttribute('prefix-icon', iconType);
}

/**
* Determines the arrow icon based on the consumer-defined `arrowDirection`.
*
* @returns The icon that represents the arrow direction.
*/
private getArrowIcon(): string {
const isHorizontal = this.orientation === DIVIDER_ORIENTATION.HORIZONTAL;
const isPositive = this.arrowDirection === DIRECTIONS.POSITIVE;

if (isHorizontal) {
return isPositive ? ARROW_ICONS.UP : ARROW_ICONS.DOWN;
}

return isPositive ? ARROW_ICONS.RIGHT : ARROW_ICONS.LEFT;
}

public override update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.update(changedProperties);

if (changedProperties.has('orientation')) {
this.setOrientation(this.orientation);
}

if (changedProperties.has('variant')) {
this.setVariant(this.variant);
}

if (
changedProperties.has('orientation')
|| changedProperties.has('arrowDirection')
|| changedProperties.has('buttonPosition')
) {
this.setGrabberButton();
}
}

/**
* Infers the type of divider based on the kind of slot present.
* @param slot - default slot of divider
*/
private inferDividerType() {
this.setAttribute('data-type', 'mdc-primary-divider');

const slot = this.shadowRoot?.querySelector('slot');
const assignedElements = slot?.assignedElements({ flatten: true }) || [];
if (assignedElements.length > 1) return;

const hasTextChild = assignedElements.some((el) => el.tagName === TEXT_TAG.toUpperCase());
const hasButtonChild = assignedElements.some((el) => el.tagName === BUTTON_TAG.toUpperCase());

if (hasTextChild && !hasButtonChild) {
this.setAttribute('data-type', 'mdc-text-divider');
} else if (!hasTextChild && hasButtonChild) {
this.setAttribute('data-type', 'mdc-grabber-divider');
this.setGrabberButton();
}
}

protected override render() {
return html`
<div></div>
<slot @slotchange=${this.inferDividerType}></slot>
<div></div>
`;
}

public static override styles: Array<CSSResult> = [...Component.styles, ...styles];
}

export default Divider;
49 changes: 49 additions & 0 deletions packages/components/src/components/divider/divider.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import utils from '../../utils/tag-name';
import { TAG_NAME as BUTTON_TAG } from '../button/button.constants';
import { TAG_NAME as TEXT_TAG } from '../text/text.constants';

const TAG_NAME = utils.constructTagName('divider');

const DIVIDER_ORIENTATION = {
HORIZONTAL: 'horizontal',
VERTICAL: 'vertical',
} as const;

const DIVIDER_VARIANT = {
SOLID: 'solid',
GRADIENT: 'gradient',
} as const;

/**
* Direction types for both the arrow and button component.
* These directions are dependent on the divider's orientation.
*/
const DIRECTIONS = {
POSITIVE: 'positive',
NEGATIVE: 'negative',
} as const;

const ARROW_ICONS = {
UP: 'arrow-up-regular',
DOWN: 'arrow-down-regular',
LEFT: 'arrow-left-regular',
RIGHT: 'arrow-right-regular',
} as const;

const DEFAULTS = {
ORIENTATION: DIVIDER_ORIENTATION.HORIZONTAL,
VARIANT: DIVIDER_VARIANT.SOLID,
ARROW_DIRECTION: DIRECTIONS.NEGATIVE,
BUTTON_DIRECTION: DIRECTIONS.NEGATIVE,
} as const;

export {
TAG_NAME,
DEFAULTS,
DIVIDER_VARIANT,
DIVIDER_ORIENTATION,
DIRECTIONS,
BUTTON_TAG,
TEXT_TAG,
ARROW_ICONS,
};
66 changes: 66 additions & 0 deletions packages/components/src/components/divider/divider.e2e-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test } from '../../../config/playwright/setup';

test.beforeEach(async ({ componentsPage }) => {
await componentsPage.mount({
html: `
<mdc-divider />
`,
});
});

test.skip('mdc-divider', async ({ componentsPage }) => {
const divider = componentsPage.page.locator('mdc-divider');

// initial check for the divider be visible on the screen:
await divider.waitFor();

/**
* ACCESSIBILITY
*/
await test.step('accessibility', async () => {
await componentsPage.accessibility.checkForA11yViolations('divider-default');
});

/**
* VISUAL REGRESSION
*/
await test.step('visual-regression', async () => {
await test.step('matches screenshot of element', async () => {
await componentsPage.visualRegression.takeScreenshot('mdc-divider', { element: divider });
});
});

/**
* ATTRIBUTES
*/
await test.step('attributes', async () => {
await test.step('attribute X should be present on component by default', async () => {
// TODO: add test here
});
});

/**
* INTERACTIONS
*/
await test.step('interactions', async () => {
await test.step('mouse/pointer', async () => {
await test.step('component should fire callback x when clicking on it', async () => {
// TODO: add test here
});
});

await test.step('focus', async () => {
await test.step('component should be focusable with tab', async () => {
// TODO: add test here
});

// add additional tests here, like tabbing through several parts of the component
});

await test.step('keyboard', async () => {
await test.step('component should fire callback x when pressing y', async () => {
// TODO: add test here
});
});
});
});
Loading

0 comments on commit 3056af0

Please sign in to comment.