Skip to content

Commit

Permalink
fix: add likeAnchor API to Card element
Browse files Browse the repository at this point in the history
  • Loading branch information
Westbrook committed Oct 11, 2021
1 parent 67e1d78 commit 5c338fb
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 31 deletions.
18 changes: 17 additions & 1 deletion packages/card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { Card } from '@spectrum-web-components/card';

## Heading

By default, the heading for an `sp-card` is applied via the `heading` attribute, which is restricted to string content only. When HTML content is desired, a slot named `heading` available for applying the heading.
By default, the heading for an `<sp-card>` is applied via the `heading` attribute, which is restricted to string content only. When HTML content is desired, a slot named `heading` available for applying the heading.

```html demo
<sp-card
Expand All @@ -62,6 +62,22 @@ By default, the heading for an `sp-card` is applied via the `heading` attribute,
</sp-card>
```

## Linking

An `<sp-card>` can be provided with an `href` attribute in order for it to act as one large anchor element. When leveraging the `href` attribute, the `download`, `target` and `rel` attributes come into play to customize the linking behavior of the element. Use them as follows:

<!-- prettier-ignore -->
```html
<sp-card
heading="Card Title"
subheading="JPG"
href="https://opensource.adobe.com/spectrum-web-components"
target="_blank"
>
<img slot="cover-photo" src="https://picsum.photos/200/300" alt="Demo Image" />
</sp-card>
```

## Variants

There are multiple card variants to choose from in Spectrum. The `variant`
Expand Down
119 changes: 89 additions & 30 deletions packages/card/src/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {
TemplateResult,
PropertyValues,
ifDefined,
query,
} from '@spectrum-web-components/base';
import { FocusVisiblePolyfillMixin } from '@spectrum-web-components/shared/src/focus-visible.js';
import { ObserveSlotPresence } from '@spectrum-web-components/shared/src/observe-slot-presence.js';
import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js';
import '@spectrum-web-components/asset/sp-asset.js';

import { ObserveSlotPresence } from '@spectrum-web-components/shared';
import { Checkbox } from '@spectrum-web-components/checkbox/src/Checkbox';
import '@spectrum-web-components/checkbox/sp-checkbox.js';
import '@spectrum-web-components/quick-actions/sp-quick-actions.js';
Expand All @@ -42,9 +44,11 @@ import detailStyles from '@spectrum-web-components/styles/detail.js';
* @slot actions - an `sp-action-menu` element outlining actions to take on the represened object
* @slot footer - Footer text
*/
export class Card extends ObserveSlotPresence(
FocusVisiblePolyfillMixin(SpectrumElement),
['[slot="cover-photo"]', '[slot="preview"]']
export class Card extends LikeAnchor(
ObserveSlotPresence(FocusVisiblePolyfillMixin(SpectrumElement), [
'[slot="cover-photo"]',
'[slot="preview"]',
])
) {
public static get styles(): CSSResultArray {
return [headingStyles, detailStyles, cardStyles];
Expand All @@ -65,6 +69,9 @@ export class Card extends ObserveSlotPresence(
@property({ type: Boolean, reflect: true })
public horizontal = false;

@query('#like-anchor')
private likeAnchor?: HTMLAnchorElement;

@property({ type: String, reflect: true })
public size?: 's';

Expand Down Expand Up @@ -92,6 +99,10 @@ export class Card extends ObserveSlotPresence(
this.addEventListener('focusout', this.handleFocusout);
}

public click(): void {
this.likeAnchor?.click();
}

private handleFocusin = (event: Event): void => {
this.focused = true;
const target = event.composedPath()[0];
Expand All @@ -112,8 +123,17 @@ export class Card extends ObserveSlotPresence(

private handleKeydown(event: KeyboardEvent): void {
const { code } = event;
if (code === 'Space') {
this.toggleSelected();
switch (code) {
case 'Space':
this.toggleSelected();
if (this.toggles) {
break;
}
case 'Enter':
case 'NumpadEnter':
if (this.href) {
this.likeAnchor?.click();
}
}
}

Expand Down Expand Up @@ -149,9 +169,35 @@ export class Card extends ObserveSlotPresence(
}
}

private stopPropagationOnHref(event: Event): void {
if (this.href) {
event.stopPropagation();
}
}

private handlePointerdown(event: Event): void {
const path = event.composedPath();
const hasAnchor = path.some(
(el) => (el as HTMLElement).localName === 'a'
);
if (hasAnchor) return;
const start = +new Date();
const handleEnd = (): void => {
const end = +new Date();
if (end - start < 200) {
this.likeAnchor?.click();
}
this.removeEventListener('pointerup', handleEnd);
};
this.addEventListener('pointerup', handleEnd);
}

protected get renderHeading(): TemplateResult {
return html`
<div class="title spectrum-Heading spectrum-Heading--sizeXS">
<div
class="title spectrum-Heading spectrum-Heading--sizeXS"
id="heading"
>
<slot name="heading">${this.heading}</slot>
</div>
`;
Expand Down Expand Up @@ -201,26 +247,6 @@ export class Card extends ObserveSlotPresence(

protected render(): TemplateResult {
return html`
${this.toggles
? html`
<sp-quick-actions class="quickActions">
<sp-checkbox
tabindex="-1"
class="checkbox"
@change=${this.handleSelectedChange}
?checked=${this.selected}
></sp-checkbox>
</sp-quick-actions>
`
: html``}
${this.variant === 'quiet' && this.size === 's'
? html`
<sp-quick-actions class="spectrum-QuickActions actions">
<slot name="actions"></slot>
</sp-quick-actions>
`
: html``}
${this.renderImage()}
<div class="body">
<div class="header">
${this.renderHeading}
Expand All @@ -229,7 +255,10 @@ export class Card extends ObserveSlotPresence(
: html``}
${this.variant !== 'quiet' || this.size !== 's'
? html`
<div class="actionButton">
<div
class="actionButton"
@pointerdown=${this.stopPropagationOnHref}
>
<slot name="actions"></slot>
</div>
`
Expand All @@ -243,17 +272,47 @@ export class Card extends ObserveSlotPresence(
`
: html``}
</div>
${this.href
? this.renderAnchor({
id: 'like-anchor',
labelledby: 'heading',
})
: html``}
${this.variant === 'standard'
? html`
<slot name="footer"></slot>
`
: html``}
${this.renderImage()}
${this.toggles
? html`
<sp-quick-actions
class="quickActions"
@pointerdown=${this.stopPropagationOnHref}
>
<sp-checkbox
class="checkbox"
@change=${this.handleSelectedChange}
?checked=${this.selected}
></sp-checkbox>
</sp-quick-actions>
`
: html``}
${this.variant === 'quiet' && this.size === 's'
? html`
<sp-quick-actions
class="spectrum-QuickActions actions"
@pointerdown=${this.stopPropagationOnHref}
>
<slot name="actions"></slot>
</sp-quick-actions>
`
: html``}
`;
}

protected firstUpdated(changes: PropertyValues): void {
super.firstUpdated(changes);
this.setAttribute('role', 'figure');
this.tabIndex = 0;
this.addEventListener('pointerdown', this.handlePointerdown);
}
}
23 changes: 23 additions & 0 deletions packages/card/src/card.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ governing permissions and limitations under the License.

@import './spectrum-card.css';

:host([href]:not([href=''])) {
cursor: pointer;
}

#like-anchor {
position: absolute;
inset: 0;
pointer-events: none;
}

.actionButton {
flex-grow: 0;
}

:host([dir='ltr']) .actionButton {
margin-left: auto;
}

:host([dir='rtl']) .actionButton {
margin-right: auto;
}

/* The description slot has a psuedo-element that also needs to receive the font styling.
We need to add the declaration to the slot as well */
slot[name='description'] {
Expand All @@ -24,6 +46,7 @@ slot[name='description'] {
#preview,
#cover-photo {
overflow: hidden;
order: -1;
}

#preview + #cover-photo {
Expand Down
63 changes: 63 additions & 0 deletions packages/card/stories/card.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '@spectrum-web-components/action-menu/sp-action-menu.js';
import '@spectrum-web-components/menu/sp-menu.js';
import '@spectrum-web-components/menu/sp-menu-item.js';
import '@spectrum-web-components/menu/sp-menu-divider.js';
import '@spectrum-web-components/link/sp-link.js';

export default {
component: 'sp-card',
Expand Down Expand Up @@ -54,6 +55,7 @@ export default {
export interface StoryArgs {
horizontal?: boolean;
size?: 's';
onClick?: ((event: Event) => void) | undefined;
}

export const Default = (args: StoryArgs): TemplateResult => {
Expand All @@ -71,6 +73,45 @@ export const Default = (args: StoryArgs): TemplateResult => {
};
Default.args = {};

export const href = (args: StoryArgs): TemplateResult => {
const { onClick } = args;
return html`
<sp-card
heading="Card Heading"
subheading="JPG"
.size=${args.size}
toggles
?horizontal=${args.horizontal}
href="https://opensource.adobe.com/spectrum-web-components"
@click=${(event: Event) => {
const composedTarget = event.composedPath()[0] as HTMLElement;
if (composedTarget.id !== 'like-anchor') return;
event.stopPropagation();
event.preventDefault();
onClick && onClick(event);
}}
>
<div slot="footer">
Footer with a
<sp-link href="https://google.com">link to Google</sp-link>
</div>
<sp-action-menu slot="actions" placement="bottom-end">
<sp-menu-item>Deselect</sp-menu-item>
<sp-menu-item>Select Inverse</sp-menu-item>
<sp-menu-item>Feather...</sp-menu-item>
<sp-menu-item>Select and Mask...</sp-menu-item>
<sp-menu-divider></sp-menu-divider>
<sp-menu-item>Save Selection</sp-menu-item>
<sp-menu-item disabled>Make Work Path</sp-menu-item>
</sp-action-menu>
<img slot="cover-photo" src=${portrait} alt="Demo Graphic" />
</sp-card>
`;
};
href.argTypes = {
onClick: { action: 'link click' },
};

export const actions = (args: StoryArgs): TemplateResult => {
return html`
<sp-card
Expand Down Expand Up @@ -258,6 +299,28 @@ smallHorizontal.args = {
size: 's',
};

export const smallHorizontalWithHREF = (args: StoryArgs): TemplateResult => {
return html`
<sp-card
.size=${args.size}
?horizontal=${args.horizontal}
heading="Card Heading"
subheading="JPG"
href="https://opensource.adobe.com/spectrum-web-components"
target="_blank"
>
<sp-icon-file-txt
slot="preview"
style="width: 36px; height: 36px;"
></sp-icon-file-txt>
</sp-card>
`;
};
smallHorizontalWithHREF.args = {
horizontal: true,
size: 's',
};

export const smallQuiet = (args: StoryArgs): TemplateResult => {
return html`
<div style="width: 115px">
Expand Down
Loading

0 comments on commit 5c338fb

Please sign in to comment.