Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Layout::Flex - Component implementation #2751

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9ba9856
initial blueprint generation + cleanup after generation
didoo Mar 5, 2025
351f687
updated template registry for `components` package
didoo Mar 5, 2025
ee33fc3
added support for `@direction` argument
didoo Mar 5, 2025
1ea4687
added support for `@isInline` argument
didoo Mar 5, 2025
1c53aed
added support for `@wrap` argument
didoo Mar 5, 2025
e22a99e
added support for `@justify` and `@align` arguments
didoo Mar 5, 2025
557c9b8
added support for `@tag` argument to support dynamic tag generation
didoo Mar 5, 2025
7c6e08b
added `Layout::Flex::Item` yielded subcomponent, with support for `@t…
didoo Mar 6, 2025
421f2ca
linting
didoo Mar 6, 2025
3f2161b
extended the `@basis` argument to accept also the `0` (numeric) value
didoo Mar 6, 2025
9c841e2
added support for `@enableCollapseBelowContentSize` argument for `Fle…
didoo Mar 6, 2025
cc0e226
added some basic examples to the `Flex` showcase page
didoo Mar 7, 2025
407e201
added support for `@gap` argument with pre-defined spacing scale
didoo Mar 7, 2025
8457ad5
added missing assertions to getters of `Flex`
didoo Mar 7, 2025
abf1c2f
linting
didoo Mar 7, 2025
20f8b5c
added comment in code
didoo Mar 10, 2025
a1b7df2
small update to how the `Flex::Item` subcomponent handles the `@basis…
didoo Mar 10, 2025
b29b31c
added integration tests for `Layout::Flex` and `Layout::Flex::Item` c…
didoo Mar 10, 2025
041523a
linting
didoo Mar 12, 2025
0c61309
replaced `Hds::Flex::Item` with `HLF.Item` where meaningful
didoo Mar 12, 2025
8cfb28d
assigned HTML tags to some of the flex items in the showcase page
didoo Mar 12, 2025
5d6af0e
shared tag-related types across `Flex` and `Flex::Item`
didoo Mar 12, 2025
a04ba5e
simplified some tests
didoo Mar 12, 2025
c8b4531
added missing assertion tests for `Hds::Flex`
didoo Mar 12, 2025
cda03e8
removed comment after decision with Kristin
didoo Mar 12, 2025
0bda2b0
simplified a bit the showcase code
didoo Mar 12, 2025
f320161
more cleanup
didoo Mar 12, 2025
df0ad0b
fixed a couple of headings
didoo Mar 13, 2025
24cb5f7
ported `gap` implementation via CSS variables from `Grid` implementation
didoo Mar 14, 2025
97c64ea
remove extra CSS class for simplification
didoo Mar 14, 2025
8e79f13
linting fixes
didoo Mar 17, 2025
11d2bbe
more linting fixes
didoo Mar 17, 2025
9c11ee4
fixed test
didoo Mar 17, 2025
757e032
small code cleanup
didoo Mar 18, 2025
3542b3c
added changeset
didoo Mar 18, 2025
566566c
added comments
didoo Mar 18, 2025
e0a7906
fixes showcase example
didoo Mar 18, 2025
c0cd637
small fixes per PR reviews/suggestions
didoo Mar 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-dragons-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashicorp/design-system-components": minor
---

`Layout::Flex` - Added `Flex` and `Flex::Item` components
2 changes: 2 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@
"./components/hds/icon-tile/index.js": "./dist/_app_/components/hds/icon-tile/index.js",
"./components/hds/icon/index.js": "./dist/_app_/components/hds/icon/index.js",
"./components/hds/interactive/index.js": "./dist/_app_/components/hds/interactive/index.js",
"./components/hds/layout/flex/index.js": "./dist/_app_/components/hds/layout/flex/index.js",
"./components/hds/layout/flex/item.js": "./dist/_app_/components/hds/layout/flex/item.js",
"./components/hds/link/inline.js": "./dist/_app_/components/hds/link/inline.js",
"./components/hds/link/standalone.js": "./dist/_app_/components/hds/link/standalone.js",
"./components/hds/menu-primitive/index.js": "./dist/_app_/components/hds/menu-primitive/index.js",
Expand Down
15 changes: 15 additions & 0 deletions packages/components/src/components/hds/layout/flex/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{!
Dynamically generating an HTML tag in Ember creates a dynamic component class (with the corresponding tagName), while rendering
a plain HTML element requires less computing cycles for Ember (you will notice it doesn't add the `ember-view` class to it).
}}
{{#if (eq this.componentTag "div")}}
<div class={{this.classNames}} ...attributes>{{yield (hash Item=(component "hds/layout/flex/item"))}}</div>
{{else}}
{{#let (element this.componentTag) as |Tag|}}
<Tag class={{this.classNames}} ...attributes>{{yield (hash Item=(component "hds/layout/flex/item"))}}</Tag>
{{/let}}
{{/if}}
165 changes: 165 additions & 0 deletions packages/components/src/components/hds/layout/flex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { assert } from '@ember/debug';

import type { ComponentLike } from '@glint/template';

import type { HdsLayoutFlexItemSignature } from './item.ts';

import {
HdsLayoutFlexDirectionValues,
HdsLayoutFlexJustifyValues,
HdsLayoutFlexAlignValues,
HdsLayoutFlexGapValues,
} from './types.ts';

import type {
HdsLayoutFlexDirections,
HdsLayoutFlexJustifys,
HdsLayoutFlexAligns,
HdsLayoutFlexGaps,
AvailableTagNames,
AvailableElements,
} from './types.ts';

export const DEFAULT_DIRECTION = HdsLayoutFlexDirectionValues.Row;
export const DIRECTIONS: string[] = Object.values(HdsLayoutFlexDirectionValues);
export const JUSTIFYS: string[] = Object.values(HdsLayoutFlexJustifyValues);
export const ALIGNS: string[] = Object.values(HdsLayoutFlexAlignValues);
export const GAPS: string[] = Object.values(HdsLayoutFlexGapValues);

export interface HdsLayoutFlexSignature {
Args: {
tag?: AvailableTagNames;
direction?: HdsLayoutFlexDirections;
justify?: HdsLayoutFlexJustifys;
align?: HdsLayoutFlexAligns;
wrap?: boolean;
gap?: HdsLayoutFlexGaps | [HdsLayoutFlexGaps, HdsLayoutFlexGaps];
isInline?: boolean;
};
Blocks: {
default: [
{
Item?: ComponentLike<HdsLayoutFlexItemSignature>;
},
];
};
Element: AvailableElements;
}

export default class HdsLayoutFlex extends Component<HdsLayoutFlexSignature> {
get componentTag(): AvailableTagNames {
return this.args.tag ?? 'div';
}

get direction(): HdsLayoutFlexDirections {
const { direction = DEFAULT_DIRECTION } = this.args;

assert(
`@direction for "Hds::Layout::Flex" must be one of the following: ${DIRECTIONS.join(
', '
)}; received: ${direction}`,
DIRECTIONS.includes(direction)
);

return direction;
}

get justify(): HdsLayoutFlexJustifys | undefined {
const { justify } = this.args;

if (justify) {
assert(
`@justify for "Hds::Layout::Flex" must be one of the following: ${JUSTIFYS.join(
', '
)}; received: ${justify}`,
JUSTIFYS.includes(justify)
);
}

return justify;
}

get align(): HdsLayoutFlexAligns | undefined {
const { align } = this.args;

if (align) {
assert(
`@align for "Hds::Layout::Flex" must be one of the following: ${ALIGNS.join(
', '
)}; received: ${align}`,
ALIGNS.includes(align)
);
}

return align;
}

get gap():
| [HdsLayoutFlexGaps]
| [HdsLayoutFlexGaps, HdsLayoutFlexGaps]
| undefined {
const { gap } = this.args;

if (gap) {
assert(
`@gap for "Hds::Layout::Flex" must be a single value or an array of two values of one of the following: ${GAPS.join(
', '
)}; received: ${gap}`,
(!Array.isArray(gap) && GAPS.includes(gap)) ||
(Array.isArray(gap) &&
gap.length === 2 &&
GAPS.includes(gap[0]) &&
GAPS.includes(gap[1]))
);
return Array.isArray(gap) ? gap : [gap];
} else {
return undefined;
}
}

get classNames() {
const classes = ['hds-layout-flex'];

// add a class based on the @direction argument
classes.push(`hds-layout-flex--direction-${this.direction}`);

// add a class based on the @justify argument
if (this.justify) {
classes.push(`hds-layout-flex--justify-content-${this.justify}`);
}

// add a class based on the @align argument
if (this.align) {
classes.push(`hds-layout-flex--align-items-${this.align}`);
}

// add a class based on the @gap argument
if (this.gap) {
if (this.gap.length === 2) {
classes.push(`hds-layout-flex--row-gap-${this.gap[0]}`);
classes.push(`hds-layout-flex--column-gap-${this.gap[1]}`);
} else if (this.gap.length === 1) {
classes.push(`hds-layout-flex--row-gap-${this.gap[0]}`);
classes.push(`hds-layout-flex--column-gap-${this.gap[0]}`);
}
}

// add a class based on the @wrap argument
if (this.args.wrap) {
classes.push('hds-layout-flex--has-wrapping');
}

// add a class based on the @isInline argument
if (this.args.isInline) {
classes.push('hds-layout-flex--is-inline');
}

return classes.join(' ');
}
}
17 changes: 17 additions & 0 deletions packages/components/src/components/hds/layout/flex/item.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{!
Dynamically generating an HTML tag in Ember creates a dynamic component class (with the corresponding tagName), while rendering
a plain HTML element requires less computing cycles for Ember (you will notice it doesn't add the `ember-view` class to it).
}}
{{#if (eq this.componentTag "div")}}
<div class={{this.classNames}} {{style this.inlineStyles}} ...attributes>{{yield}}</div>
{{else}}
{{#let (element this.componentTag) as |Tag|}}<Tag
class={{this.classNames}}
{{style this.inlineStyles}}
...attributes
>{{yield}}</Tag>{{/let}}
{{/if}}
90 changes: 90 additions & 0 deletions packages/components/src/components/hds/layout/flex/item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';

import type { AvailableTagNames, AvailableElements } from './types.ts';

export interface HdsLayoutFlexItemSignature {
Args: {
tag?: AvailableTagNames;
basis?: string | 0;
grow?: boolean | number | string;
shrink?: boolean | number | string;
// TODO final name TBD
enableCollapseBelowContentSize?: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KristinLBradley I am open to alternatives for this argument (and to discuss if we want to keep it or not)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the possible use cases & do they seem common?

This seems like a confusing feature that perhaps could be left up to consumers to implement something for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@KristinLBradley KristinLBradley Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'm more wondering if there are common use cases for not wanting to set it to true.

(i.e. for not including min-width: 0;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any more thoughts around this one?

};
Blocks: {
default: [];
};
Element: AvailableElements;
}

export default class HdsLayoutFlexItem extends Component<HdsLayoutFlexItemSignature> {
get componentTag(): AvailableTagNames {
return this.args.tag ?? 'div';
}

get inlineStyles(): Record<string, unknown> {
const inlineStyles: {
'flex-basis'?: string;
'flex-grow'?: string;
'flex-shrink'?: string;
} = {};

// we handle all non-zero cases of `basis` values via inline styles
if (typeof this.args.basis === 'string') {
inlineStyles['flex-basis'] = this.args.basis;
}

// we handle non-standard cases of `grow` values via inline styles
if (typeof this.args.grow === 'number' && this.args.grow > 1) {
// the `{{style}}` modifier accepts only strings
inlineStyles['flex-grow'] = this.args.grow.toString();
} else if (typeof this.args.grow === 'string') {
inlineStyles['flex-grow'] = this.args.grow;
}

// we handle non-standard cases of `shrink` values via inline styles
if (typeof this.args.shrink === 'number' && this.args.shrink > 1) {
// the `{{style}}` modifier accepts only strings
inlineStyles['flex-shrink'] = this.args.shrink.toString();
} else if (typeof this.args.shrink === 'string') {
inlineStyles['flex-shrink'] = this.args.shrink;
}

return inlineStyles;
}

get classNames() {
const classes = ['hds-layout-flex-item'];

// add a class based on the @basis argument (if set to `0`)
if (this.args.basis === 0) {
classes.push('hds-layout-flex-item--basis-0');
}

// add a class based on the @grow argument (if set to `0/1` or `true/false`)
if (this.args.grow === 0 || this.args.grow === false) {
classes.push('hds-layout-flex-item--grow-0');
} else if (this.args.grow === 1 || this.args.grow === true) {
classes.push('hds-layout-flex-item--grow-1');
}

// add a class based on the @shrink argument (if set to `0/1` or `true/false`)
if (this.args.shrink === 0 || this.args.shrink === false) {
classes.push('hds-layout-flex-item--shrink-0');
} else if (this.args.shrink === 1 || this.args.shrink === true) {
classes.push('hds-layout-flex-item--shrink-1');
}

// add a class based on the @enableCollapseBelowContentSize argument (applies a `min-width: 0`)
if (this.args.enableCollapseBelowContentSize) {
classes.push('hds-layout-flex-item--enable-collapse-below-content-size');
}

return classes.join(' ');
}
}
49 changes: 49 additions & 0 deletions packages/components/src/components/hds/layout/flex/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

export enum HdsLayoutFlexDirectionValues {
Row = 'row',
Column = 'column',
}

export type HdsLayoutFlexDirections = `${HdsLayoutFlexDirectionValues}`;

export enum HdsLayoutFlexJustifyValues {
Start = 'start',
Center = 'center',
End = 'end',
SpaceBetween = 'space-between',
SpaceAround = 'space-around',
SpaceEvenly = 'space-evenly',
}

export type HdsLayoutFlexJustifys = `${HdsLayoutFlexJustifyValues}`;

export enum HdsLayoutFlexAlignValues {
Start = 'start',
Center = 'center',
End = 'end',
Stretch = 'stretch',
}

export type HdsLayoutFlexAligns = `${HdsLayoutFlexAlignValues}`;

export enum HdsLayoutFlexGapValues {
'Four' = '4',
'Eight' = '8',
'Twelve' = '12',
'Sixteen' = '16',
'TwentyFour' = '24',
'ThirtyTwo' = '32',
'FortyEight' = '48',
}

export type HdsLayoutFlexGaps = `${HdsLayoutFlexGapValues}`;

// A list of all existing tag names in the HTMLElementTagNameMap interface
export type AvailableTagNames = keyof HTMLElementTagNameMap;
// A union of all types in the HTMLElementTagNameMap interface
export type AvailableElements =
HTMLElementTagNameMap[keyof HTMLElementTagNameMap];
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
@use "../components/form"; // multiple components
@use "../components/icon";
@use "../components/icon-tile";
@use "../components/layout"; // multiple components
@use "../components/link"; // multiple components
@use "../components/menu-primitive";
@use "../components/modal";
Expand Down
Loading