Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@
"./modifiers/hds-code-editor/palettes/hds-dark-palette.js": "./dist/_app_/modifiers/hds-code-editor/palettes/hds-dark-palette.js",
"./modifiers/hds-code-editor/themes/hds-dark-theme.js": "./dist/_app_/modifiers/hds-code-editor/themes/hds-dark-theme.js",
"./modifiers/hds-code-editor/types.js": "./dist/_app_/modifiers/hds-code-editor/types.js",
"./modifiers/hds-lifecycle.js": "./dist/_app_/modifiers/hds-lifecycle.js",
"./modifiers/hds-register-element.js": "./dist/_app_/modifiers/hds-register-element.js",
"./modifiers/hds-register-event.js": "./dist/_app_/modifiers/hds-register-event.js",
"./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js",
"./services/hds-intl.js": "./dist/_app_/services/hds-intl.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<div
class="hds-advanced-table__container
{{(if this.isStickyHeaderPinned 'hds-advanced-table__container--header-is-pinned')}}"
{{did-update this.setupTableModelData @columns @model @sortBy @sortOrder}}
{{did-update this.updateTableModelColumnOrder @columnOrder}}
{{this._syncTableData columns=@columns model=@model sortBy=@sortBy sortOrder=@sortOrder}}
{{this._syncColumnOrder columnOrder=@columnOrder}}
...attributes
>
{{! Caption }}
Expand Down
85 changes: 64 additions & 21 deletions packages/components/src/components/hds/advanced-table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking';
import { guidFor } from '@ember/object/internals';
import { service } from '@ember/service';
import { modifier } from 'ember-modifier';
import { schedule } from '@ember/runloop';
import HdsAdvancedTableTableModel from './models/table.ts';

import type Owner from '@ember/owner';
Expand Down Expand Up @@ -196,6 +197,25 @@ export interface HdsAdvancedTableSignature {
Element: HTMLDivElement;
}

interface HdsAdvancedTableSyncTableDataModifierSignature {
Element: HdsAdvancedTableSignature['Element'];
Args: {
Named: Pick<
HdsAdvancedTableSignature['Args'],
'columns' | 'model' | 'sortBy' | 'sortOrder'
>;
};
}

interface HdsAdvancedTableSyncColumnOrderModifierSignature {
Element: HdsAdvancedTableSignature['Element'];
Args: {
Named: {
columnOrder?: HdsAdvancedTableSignature['Args']['columnOrder'];
};
};
}

export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignature> {
@service hdsIntl!: HdsIntlService;

Expand Down Expand Up @@ -426,6 +446,50 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
return classes.join(' ');
}

private _syncTableData = (() => {
let isFirstRun = true;

return modifier<HdsAdvancedTableSyncTableDataModifierSignature>(
(_element, _positional, { columns, model, sortBy, sortOrder }) => {
// eslint-disable-next-line ember/no-runloop
schedule('afterRender', (): void => {
if (isFirstRun) {
isFirstRun = false;
return;
}
this._tableModel.setupData({
columns,
model,
sortBy,
sortOrder,
});
});
}
);
})();

private _syncColumnOrder = (() => {
let isFirstRun = true;

return modifier<HdsAdvancedTableSyncColumnOrderModifierSignature>(
(_element, _positional, { columnOrder }) => {
// eslint-disable-next-line ember/no-runloop
schedule('afterRender', (): void => {
if (isFirstRun) {
isFirstRun = false;
return;
}

if (columnOrder === undefined) {
return;
}

this._tableModel.columnOrder = columnOrder;
});
}
);
})();

private _registerGridElement = modifier((element: HTMLDivElement) => {
this._tableModel.gridElement = element;
});
Expand Down Expand Up @@ -613,27 +677,6 @@ export default class HdsAdvancedTable extends Component<HdsAdvancedTableSignatur
});
}

@action
setupTableModelData(): void {
const { columns, model, sortBy, sortOrder } = this.args;

this._tableModel.setupData({
columns,
model,
sortBy,
sortOrder,
});
}

@action
updateTableModelColumnOrder(): void {
if (this.args.columnOrder === undefined) {
return;
}

this._tableModel.columnOrder = this.args.columnOrder;
}

@action
onSelectionAllChange(): void {
this._selectableRows.forEach((row) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<div class="hds-alert__description hds-font-weight-regular hds-foreground-primary" ...attributes>{{yield}}</div>
<div
class="hds-alert__description hds-font-weight-regular hds-foreground-primary"
{{this._registerElement}}
...attributes
>{{yield}}</div>
17 changes: 12 additions & 5 deletions packages/components/src/components/hds/alert/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
* SPDX-License-Identifier: MPL-2.0
*/

import TemplateOnlyComponent from '@ember/component/template-only';
import Component from '@glimmer/component';
import { modifier } from 'ember-modifier';

export interface HdsAlertDescriptionSignature {
Args: {
onInsert: (element: HdsAlertDescriptionSignature['Element']) => void;
};
Blocks: {
default: [];
};
Element: HTMLDivElement;
}

const HdsAlertDescription =
TemplateOnlyComponent<HdsAlertDescriptionSignature>();

export default HdsAlertDescription;
export default class HdsAlertDescription extends Component<HdsAlertDescriptionSignature> {
private _registerElement = modifier(
(element: HdsAlertDescriptionSignature['Element']) => {
this.args.onInsert(element);
}
);
}
11 changes: 5 additions & 6 deletions packages/components/src/components/hds/alert/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
}}
<div
class={{this.classNames}}
role={{this._role}}
aria-live={{if this._role "polite"}}
role={{this.role}}
aria-live={{if this.role "polite"}}
aria-labelledby={{this._ariaLabelledBy}}
{{did-insert this.didInsert}}
...attributes
>
{{#if this.icon}}
Expand All @@ -16,10 +15,10 @@
</div>
{{/if}}

<div class="hds-alert__content">
<div class="hds-alert__content" {{this._registerActions}}>
<div class="hds-alert__text {{if (eq @type 'compact') 'hds-typography-body-100' 'hds-typography-body-200'}}">
{{yield (hash Title=(component "hds/alert/title"))}}
{{yield (hash Description=(component "hds/alert/description"))}}
{{yield (hash Title=(component "hds/alert/title" onInsert=this._registerTitle))}}
{{yield (hash Description=(component "hds/alert/description" onInsert=this._registerDescription))}}
</div>

<div class="hds-alert__actions">
Expand Down
90 changes: 60 additions & 30 deletions packages/components/src/components/hds/alert/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { action } from '@ember/object';
import { assert } from '@ember/debug';
import { guidFor } from '@ember/object/internals';
import { tracked } from '@glimmer/tracking';
import { modifier } from 'ember-modifier';

import { HdsAlertColorValues, HdsAlertTypeValues } from './types.ts';

Expand All @@ -16,6 +17,8 @@ import type HdsButtonComponent from '../button';
import type HdsLinkStandaloneComponent from '../link/standalone';
import type { HdsYieldSignature } from '../yield';
import type { HdsAlertColors, HdsAlertTypes } from './types.ts';
import type HdsAlertTitleComponent from './title.ts';
import type HdsAlertDescriptionComponent from './description.ts';
import type { HdsAlertTitleSignature } from './title.ts';
import type { HdsAlertDescriptionSignature } from './description.ts';
import type { HdsIconSignature } from '../icon';
Expand All @@ -25,6 +28,12 @@ export const TYPES: HdsAlertTypes[] = Object.values(HdsAlertTypeValues);
export const DEFAULT_COLOR: HdsAlertColors = HdsAlertColorValues.Neutral;
export const COLORS: HdsAlertColors[] = Object.values(HdsAlertColorValues);

const SEMANTIC_COLORS: HdsAlertColors[] = [
HdsAlertColorValues.Warning,
HdsAlertColorValues.Critical,
HdsAlertColorValues.Success,
];

export const MAPPING_COLORS_TO_ICONS = {
[HdsAlertColorValues.Neutral]: 'info',
[HdsAlertColorValues.Highlight]: 'info',
Expand All @@ -33,10 +42,6 @@ export const MAPPING_COLORS_TO_ICONS = {
[HdsAlertColorValues.Critical]: 'alert-diamond',
} as const;

const CONTENT_ELEMENT_SELECTOR = '.hds-alert__content';
const TITLE_ELEMENT_SELECTOR = '.hds-alert__title';
const DESCRIPTION_ELEMENT_SELECTOR = '.hds-alert__description';

export interface HdsAlertSignature {
Args: {
type: HdsAlertTypes;
Expand All @@ -48,8 +53,11 @@ export interface HdsAlertSignature {
Blocks: {
default: [
{
Title?: ComponentLike<HdsAlertTitleSignature>;
Description?: ComponentLike<HdsAlertDescriptionSignature>;
Title?: WithBoundArgs<typeof HdsAlertTitleComponent, 'onInsert'>;
Description?: WithBoundArgs<
typeof HdsAlertDescriptionComponent,
'onInsert'
>;
Generic?: ComponentLike<HdsYieldSignature>;
LinkStandalone?: WithBoundArgs<
typeof HdsLinkStandaloneComponent,
Expand All @@ -65,6 +73,10 @@ export interface HdsAlertSignature {
export default class HdsAlert extends Component<HdsAlertSignature> {
@tracked private _role?: string;
@tracked private _ariaLabelledBy?: string;
@tracked private _actions: NodeListOf<Element> | null = null;
@tracked private _titleElement?: HdsAlertTitleSignature['Element'];
@tracked
private _descriptionElement?: HdsAlertDescriptionSignature['Element'];

constructor(owner: Owner, args: HdsAlertSignature['Args']) {
super(owner, args);
Expand All @@ -91,6 +103,11 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
return color;
}

// an Alert which actually alerts users (has role="alert" & aria-live="polite") as opposed to an informational or promo "alert"
get isSemantic(): boolean {
return SEMANTIC_COLORS.includes(this.color);
}

// The name of the icon to be used.
get icon(): HdsIconSignature['Args']['name'] | false {
const { icon } = this.args;
Expand Down Expand Up @@ -150,33 +167,46 @@ export default class HdsAlert extends Component<HdsAlertSignature> {
return classes.join(' ');
}

@action
didInsert(element: HTMLDivElement): void {
const actions = element.querySelectorAll(
`${CONTENT_ELEMENT_SELECTOR} button, ${CONTENT_ELEMENT_SELECTOR} a`
);
get role(): 'alertdialog' | 'alert' | undefined {
if (this.isSemantic && this._actions && this._actions.length > 0) {
return 'alertdialog';
} else if (this.isSemantic) {
return 'alert';
} else {
return undefined;
}
}

// an Alert which actually alerts users (has role="alert" & aria-live="polite") as opposed to an informational or promo "alert"
const isSemanticAlert: boolean =
this.color === 'warning' ||
this.color === 'critical' ||
this.color === 'success';
get labelElement(): HdsAlertTitleSignature['Element'] | undefined {
return this._titleElement ?? this._descriptionElement;
}

if (isSemanticAlert && actions.length) {
this._role = 'alertdialog';
} else if (isSemanticAlert) {
this._role = 'alert';
}
private _registerActions = modifier((element: HTMLDivElement) => {
this._actions = element.querySelectorAll('button, a');
});

// `alertdialog` must have an accessible name so we use either the
// title or the description as label for the alert
const label =
element.querySelector(TITLE_ELEMENT_SELECTOR) ||
element.querySelector(DESCRIPTION_ELEMENT_SELECTOR);
if (label) {
const labelId = label.getAttribute('id') || guidFor(element);
label.setAttribute('id', labelId);
this._ariaLabelledBy = labelId;
private _setLabelElementId() {
if (this.labelElement === undefined) {
return;
}

const labelId =
this.labelElement.getAttribute('id') ?? guidFor(this.labelElement);

this.labelElement.setAttribute('id', labelId);

this._ariaLabelledBy = labelId;
}

@action
_registerTitle(element: HdsAlertTitleSignature['Element']): void {
this._titleElement = element;
this._setLabelElementId();
}

@action
_registerDescription(element: HdsAlertDescriptionSignature['Element']): void {
this._descriptionElement = element;
this._setLabelElementId();
}
}
1 change: 1 addition & 0 deletions packages/components/src/components/hds/alert/title.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
{{! IMPORTANT: we removed any extra newlines before/after the `let` to reduce the issues with unexpected whitespaces (see https://github.com/hashicorp/design-system/pull/1652) }}
{{#let (element this.componentTag) as |Tag|}}<Tag
class="hds-alert__title hds-typography-body-200 hds-font-weight-semibold"
{{this._registerElement}}
...attributes
>{{yield}}</Tag>{{/let}}
14 changes: 14 additions & 0 deletions packages/components/src/components/hds/alert/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
*/

import Component from '@glimmer/component';
import { modifier } from 'ember-modifier';
import { HdsAlertTitleTagValues } from './types.ts';

import type { HdsAlertTitleTags } from './types';

export interface HdsAlertTitleSignature {
Args: {
tag?: HdsAlertTitleTags;
onInsert: (element: HdsAlertTitleSignature['Element']) => void;
};
Blocks: {
default: [];
Expand All @@ -20,4 +24,14 @@ export default class HdsAlertTitle extends Component<HdsAlertTitleSignature> {
get componentTag(): HdsAlertTitleTags {
return this.args.tag ?? HdsAlertTitleTagValues.Div;
}

private _registerElement = modifier(
(element: HdsAlertTitleSignature['Element']) => {
const { onInsert } = this.args;

if (typeof onInsert === 'function') {
onInsert(element);
}
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
(hash
Item=(component "hds/app-side-nav/list/item")
BackLink=(component "hds/app-side-nav/list/back-link")
Title=(component "hds/app-side-nav/list/title" didInsertTitle=this.didInsertTitle)
Title=(component "hds/app-side-nav/list/title" registerTitleId=this._registerTitleId)
Link=(component "hds/app-side-nav/list/link")
)
}}
Expand Down
Loading