Skip to content

Commit

Permalink
Merge branch 'v1/contrib' into feature/relative-time
Browse files Browse the repository at this point in the history
  • Loading branch information
nielslyngsoe authored Nov 20, 2024
2 parents 657265c + c305dfc commit 67d056d
Show file tree
Hide file tree
Showing 30 changed files with 690 additions and 291 deletions.
20 changes: 13 additions & 7 deletions packages/uui-base/lib/mixins/SelectableMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export const SelectableMixin = <T extends Constructor<LitElement>>(
const oldVal = this._selectable;
this._selectable = newVal;
// Potentially problematic as a component might need focus for another feature when not selectable:
if (!this.selectableTarget) {
// If not selectable target, then make it self selectable. (A selectable target should be made focusable by the component itself)
if (this.selectableTarget === this) {
// If the selectable target, then make it self selectable. (A different selectable target should be made focusable by the component itself)
this.setAttribute('tabindex', `${newVal ? '0' : '-1'}`);
}
this.requestUpdate('selectable', oldVal);
Expand All @@ -80,10 +80,16 @@ export const SelectableMixin = <T extends Constructor<LitElement>>(
}

private handleSelectKeydown = (e: KeyboardEvent) => {
//if (e.composedPath().indexOf(this.selectableTarget) !== -1) {
if (this.selectableTarget === this) {
if (e.key !== ' ' && e.key !== 'Enter') return;
this._toggleSelect();
const composePath = e.composedPath();
if (
(this._selectable || (this.deselectable && this.selected)) &&
composePath.indexOf(this.selectableTarget) === 0
) {
if (this.selectableTarget === this) {
if (e.code !== 'Space' && e.code !== 'Enter') return;
this._toggleSelect();
e.preventDefault();
}
}
};

Expand Down Expand Up @@ -112,7 +118,7 @@ export const SelectableMixin = <T extends Constructor<LitElement>>(
}

private _toggleSelect() {
// Only allow for select-interaction if selectable is true. Deselectable is ignorered in this case, we do not want a DX where only deselection is a possibility..
// Only allow for select-interaction if selectable is true. Deselectable is ignored in this case, we do not want a DX where only deselection is a possibility..
if (!this.selectable) return;
if (this.deselectable === false) {
this._select();
Expand Down
4 changes: 2 additions & 2 deletions packages/uui-boolean-input/lib/uui-boolean-input.element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ export abstract class UUIBooleanInputElement extends UUIFormControlMixin(
this._value = 'on';
}
this.inputRole = inputRole;
this.addEventListener('keypress', this._onKeypress);
this.addEventListener('keydown', this.#onKeyDown);
}

protected getFormElement(): HTMLInputElement {
return this._input;
}

private _onKeypress(e: KeyboardEvent): void {
#onKeyDown(e: KeyboardEvent): void {
if (e.key == 'Enter') {
this.submit();
}
Expand Down
4 changes: 2 additions & 2 deletions packages/uui-boolean-input/lib/uui-boolean-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ describe('BooleanInputBaseElement in a Form', () => {

describe('submit', () => {
it('should submit when pressing enter', async () => {
const listener = oneEvent(formElement, 'submit', false);
element.dispatchEvent(new KeyboardEvent('keypress', { key: 'Enter' }));
const listener = oneEvent(formElement, 'submit');
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));

const event = await listener;
expect(event).to.exist;
Expand Down
31 changes: 24 additions & 7 deletions packages/uui-card-block-type/lib/uui-card-block-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('UUICardBlockTypeElement', () => {
describe('events', () => {
describe('open', () => {
it('emits a open event when open-part is clicked', async () => {
const listener = oneEvent(element, UUICardEvent.OPEN, false);
const listener = oneEvent(element, UUICardEvent.OPEN);
const infoElement: HTMLElement | null =
element.shadowRoot!.querySelector('#open-part');
infoElement?.click();
Expand All @@ -93,25 +93,42 @@ describe('UUICardBlockTypeElement', () => {
it('emits a selected event when selectable', async () => {
element.selectable = true;
await elementUpdated(element);
const listener = oneEvent(element, UUISelectableEvent.SELECTED, false);
const listener = oneEvent(element, UUISelectableEvent.SELECTED);
element.click();
const event = await listener;
expect(event).to.exist;
expect(event.type).to.equal(UUISelectableEvent.SELECTED);
expect(element.selected).to.be.true;
});

it('can be selected with keyboard', async () => {
element.selectable = true;
await elementUpdated(element);
const listener = oneEvent(element, UUISelectableEvent.SELECTED);
element.dispatchEvent(new KeyboardEvent('keydown', { code: 'Enter' }));
const event = await listener;
expect(event).to.exist;
expect(event.type).to.equal(UUISelectableEvent.SELECTED);
expect(element.selected).to.be.true;

const unselectedListener = oneEvent(
element,
UUISelectableEvent.DESELECTED,
);
element.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' }));
const event2 = await unselectedListener;
expect(event2).to.exist;
expect(event2.type).to.equal(UUISelectableEvent.DESELECTED);
expect(element.selected).to.be.false;
});
});

describe('deselect', () => {
it('emits a deselected event when preselected', async () => {
element.selectable = true;
element.selected = true;
await elementUpdated(element);
const listener = oneEvent(
element,
UUISelectableEvent.DESELECTED,
false,
);
const listener = oneEvent(element, UUISelectableEvent.DESELECTED);
element.click();
const event = await listener;
expect(event).to.exist;
Expand Down
111 changes: 85 additions & 26 deletions packages/uui-card-content-node/lib/uui-card-content-node.element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,30 @@ export class UUICardContentNodeElement extends UUICardElement {
@property({ type: String })
name = '';

/**
* Node details
* @type {string}
* @attr
* @default ''
*/
@property({ type: String })
detail = '';

@state()
private _iconSlotHasContent = false;

protected fallbackIcon =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M396.441 138.878l-83.997-83.993-7.331-7.333H105.702v416.701h298.071V146.214l-7.332-7.336zM130.74 439.217V72.591h141.613c37.201 0 19.274 88.18 19.274 88.18s86-20.901 87.104 18.534v259.912H130.74z"></path></svg>';
protected fallbackIcon = `<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
id="icon">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
</svg>`;

private _onSlotIconChange(event: Event) {
this._iconSlotHasContent =
Expand All @@ -41,18 +60,37 @@ export class UUICardContentNodeElement extends UUICardElement {
return html`<uui-icon .svg="${this.fallbackIcon}"></uui-icon>`;
}

protected renderDetail() {
return html`<small id="detail"
>${this.detail}<slot name="detail"></slot></small
><slot id="default"></slot>`;
}

#renderContent() {
return html`
<span id="content">
<span id="item">
<span id="icon">
<slot name="icon" @slotchange=${this._onSlotIconChange}></slot>
${this._iconSlotHasContent === false
? this._renderFallbackIcon()
: ''}
</span>
<div id="name">${this.name}<slot name="name"></slot></div>
</span>
${this.renderDetail()}
</span>
`;
}

#renderButton() {
return html`<div
return html`<button
id="open-part"
tabindex=${this.disabled ? (nothing as any) : 0}
@click=${this.handleOpenClick}
@keydown=${this.handleOpenKeydown}>
<span id="icon">
<slot name="icon" @slotchange=${this._onSlotIconChange}></slot>
${this._iconSlotHasContent === false ? this._renderFallbackIcon() : ''}
</span>
<span id="name"> ${this.name} </span>
</div>`;
${this.#renderContent()}
</button>`;
}

#renderLink() {
Expand All @@ -67,11 +105,7 @@ export class UUICardContentNodeElement extends UUICardElement {
this.target === '_blank' ? 'noopener noreferrer' : undefined,
),
)}>
<span id="icon">
<slot name="icon" @slotchange=${this._onSlotIconChange}></slot>
${this._iconSlotHasContent === false ? this._renderFallbackIcon() : ''}
</span>
<span id="name"> ${this.name} </span>
${this.#renderContent()}
</a>`;
}

Expand All @@ -81,7 +115,6 @@ export class UUICardContentNodeElement extends UUICardElement {
<!-- Select border must be right after #open-part -->
<div id="select-border"></div>
<slot></slot>
<slot name="tag"></slot>
<slot name="actions"></slot>
`;
Expand All @@ -94,7 +127,6 @@ export class UUICardContentNodeElement extends UUICardElement {
min-width: 250px;
flex-direction: column;
justify-content: space-between;
padding: var(--uui-size-3) var(--uui-size-4);
}
slot[name='tag'] {
Expand Down Expand Up @@ -132,30 +164,57 @@ export class UUICardContentNodeElement extends UUICardElement {
line-height: calc(2 * var(--uui-size-3));
}
#icon {
font-size: 1.2em;
margin-right: var(--uui-size-1);
}
#open-part {
display: flex;
position: relative;
font-weight: 700;
align-items: center;
cursor: pointer;
flex-grow: 1;
padding: var(--uui-size-space-4) var(--uui-size-space-5);
}
#content {
align-self: stretch;
display: flex;
flex-direction: column;
}
:host([disabled]) #open-part {
#item {
position: relative;
display: flex;
align-self: stretch;
line-height: normal;
align-items: center;
margin-top: var(--uui-size-1);
}
#icon {
font-size: 1.2em;
margin-right: var(--uui-size-1);
}
:host([selectable]) #open-part {
padding: 0;
margin: var(--uui-size-space-4) var(--uui-size-space-5);
}
:host([disabled]) #name {
pointer-events: none;
}
#open-part:hover {
:host(:not([disabled])) #open-part:hover #icon {
color: var(--uui-color-interactive-emphasis);
}
:host(:not([disabled])) #open-part:hover #name {
text-decoration: underline;
color: var(--uui-color-interactive-emphasis);
}
#name {
margin-top: 4px;
:host(:not([disabled])) #open-part:hover #detail {
color: var(--uui-color-interactive-emphasis);
}
:host(:not([disabled])) #open-part:hover #default {
color: var(--uui-color-interactive-emphasis);
}
`,
];
Expand Down
33 changes: 25 additions & 8 deletions packages/uui-card-content-node/lib/uui-card-content-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('UUICardContentNodeElement', () => {
describe('events', () => {
describe('open', () => {
it('emits a open event when info is clicked', async () => {
const listener = oneEvent(element, UUICardEvent.OPEN, false);
const listener = oneEvent(element, UUICardEvent.OPEN);
const infoElement: HTMLElement | null =
element.shadowRoot!.querySelector('#open-part');
infoElement?.click();
Expand All @@ -85,7 +85,7 @@ describe('UUICardContentNodeElement', () => {
});

it('emits a open event when icon is clicked', async () => {
const listener = oneEvent(element, UUICardEvent.OPEN, false);
const listener = oneEvent(element, UUICardEvent.OPEN);
const iconElement: HTMLElement | null =
element.shadowRoot!.querySelector('#icon');
iconElement?.click();
Expand All @@ -99,25 +99,42 @@ describe('UUICardContentNodeElement', () => {
it('emits a selected event when selectable', async () => {
element.selectable = true;
await elementUpdated(element);
const listener = oneEvent(element, UUISelectableEvent.SELECTED, false);
const listener = oneEvent(element, UUISelectableEvent.SELECTED);
element.click();
const event = await listener;
expect(event).to.exist;
expect(event.type).to.equal(UUISelectableEvent.SELECTED);
expect(element.selected).to.be.true;
});

it('can be selected with keyboard', async () => {
element.selectable = true;
await elementUpdated(element);
const listener = oneEvent(element, UUISelectableEvent.SELECTED);
element.dispatchEvent(new KeyboardEvent('keydown', { code: 'Enter' }));
const event = await listener;
expect(event).to.exist;
expect(event.type).to.equal(UUISelectableEvent.SELECTED);
expect(element.selected).to.be.true;

const unselectedListener = oneEvent(
element,
UUISelectableEvent.DESELECTED,
);
element.dispatchEvent(new KeyboardEvent('keydown', { code: 'Space' }));
const event2 = await unselectedListener;
expect(event2).to.exist;
expect(event2.type).to.equal(UUISelectableEvent.DESELECTED);
expect(element.selected).to.be.false;
});
});

describe('deselect', () => {
it('emits a deselected event when preselected', async () => {
element.selectable = true;
element.selected = true;
await elementUpdated(element);
const listener = oneEvent(
element,
UUISelectableEvent.DESELECTED,
false,
);
const listener = oneEvent(element, UUISelectableEvent.DESELECTED);
element.click();
const event = await listener;
expect(event).to.exist;
Expand Down
Loading

0 comments on commit 67d056d

Please sign in to comment.