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

feat(component): create link component #502

Merged
merged 6 commits into from
Jul 30, 2024
Merged
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: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/core/loader/cdn.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports = require('../dist/cjs/loader.cjs.js');

module.exports = require('../dist/cjs/loader.cjs.js');
module.exports.applyPolyfills = function() { return Promise.resolve() };
4 changes: 3 additions & 1 deletion packages/core/loader/index.cjs.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports = require('../dist/cjs/loader.cjs.js');

module.exports = require('../dist/cjs/loader.cjs.js');
module.exports.applyPolyfills = function() { return Promise.resolve() };
3 changes: 0 additions & 3 deletions packages/core/loader/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ export interface CustomElementsDefineOptions {
rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;
}
export declare function defineCustomElements(win?: Window, opts?: CustomElementsDefineOptions): void;
/**
* @deprecated
*/
export declare function applyPolyfills(): Promise<void>;

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/core/loader/index.es2017.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from '../dist/esm/loader.js';

export * from '../dist/esm/polyfills/index.js';
export * from '../dist/esm/loader.js';
4 changes: 3 additions & 1 deletion packages/core/loader/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@

(function(){if("undefined"!==typeof window&&void 0!==window.Reflect&&void 0!==window.customElements){var a=HTMLElement;window.HTMLElement=function(){return Reflect.construct(a,[],this.constructor)};HTMLElement.prototype=a.prototype;HTMLElement.prototype.constructor=HTMLElement;Object.setPrototypeOf(HTMLElement,a)}})();
export * from '../dist/esm/loader.js';
export * from '../dist/esm/polyfills/index.js';
export * from '../dist/esm/loader.js';
33 changes: 33 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export namespace Components {
"type": TextFieldTypes;
"value"?: IonTypes.IonInput['value'];
}
interface AtomLink {
"color": 'primary' | 'secondary';
"type": 'anchor' | 'button';
}
interface AtomListSlider {
"centralized": boolean;
"hasNavigation": boolean;
Expand Down Expand Up @@ -247,6 +251,10 @@ export interface AtomInputCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLAtomInputElement;
}
export interface AtomLinkCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLAtomLinkElement;
}
export interface AtomListSliderCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLAtomListSliderElement;
Expand Down Expand Up @@ -380,6 +388,23 @@ declare global {
prototype: HTMLAtomInputElement;
new (): HTMLAtomInputElement;
};
interface HTMLAtomLinkElementEventMap {
"click": any;
}
interface HTMLAtomLinkElement extends Components.AtomLink, HTMLStencilElement {
addEventListener<K extends keyof HTMLAtomLinkElementEventMap>(type: K, listener: (this: HTMLAtomLinkElement, ev: AtomLinkCustomEvent<HTMLAtomLinkElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLAtomLinkElementEventMap>(type: K, listener: (this: HTMLAtomLinkElement, ev: AtomLinkCustomEvent<HTMLAtomLinkElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLAtomLinkElement: {
prototype: HTMLAtomLinkElement;
new (): HTMLAtomLinkElement;
};
interface HTMLAtomListSliderElementEventMap {
"clickNext": any;
"clickPrev": any;
Expand Down Expand Up @@ -464,6 +489,7 @@ declare global {
"atom-grid": HTMLAtomGridElement;
"atom-icon": HTMLAtomIconElement;
"atom-input": HTMLAtomInputElement;
"atom-link": HTMLAtomLinkElement;
"atom-list-slider": HTMLAtomListSliderElement;
"atom-list-slider-item": HTMLAtomListSliderItemElement;
"atom-select": HTMLAtomSelectElement;
Expand Down Expand Up @@ -608,6 +634,11 @@ declare namespace LocalJSX {
"type"?: TextFieldTypes;
"value"?: IonTypes.IonInput['value'];
}
interface AtomLink {
"color"?: 'primary' | 'secondary';
"onClick"?: (event: AtomLinkCustomEvent<any>) => void;
"type"?: 'anchor' | 'button';
}
interface AtomListSlider {
"centralized"?: boolean;
"hasNavigation"?: boolean;
Expand Down Expand Up @@ -716,6 +747,7 @@ declare namespace LocalJSX {
"atom-grid": AtomGrid;
"atom-icon": AtomIcon;
"atom-input": AtomInput;
"atom-link": AtomLink;
"atom-list-slider": AtomListSlider;
"atom-list-slider-item": AtomListSliderItem;
"atom-select": AtomSelect;
Expand All @@ -739,6 +771,7 @@ declare module "@stencil/core" {
"atom-grid": LocalJSX.AtomGrid & JSXBase.HTMLAttributes<HTMLAtomGridElement>;
"atom-icon": LocalJSX.AtomIcon & JSXBase.HTMLAttributes<HTMLAtomIconElement>;
"atom-input": LocalJSX.AtomInput & JSXBase.HTMLAttributes<HTMLAtomInputElement>;
"atom-link": LocalJSX.AtomLink & JSXBase.HTMLAttributes<HTMLAtomLinkElement>;
"atom-list-slider": LocalJSX.AtomListSlider & JSXBase.HTMLAttributes<HTMLAtomListSliderElement>;
"atom-list-slider-item": LocalJSX.AtomListSliderItem & JSXBase.HTMLAttributes<HTMLAtomListSliderItemElement>;
"atom-select": LocalJSX.AtomSelect & JSXBase.HTMLAttributes<HTMLAtomSelectElement>;
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/components/link/link.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@import '~@atomium/scss-utils/index';

:host {
display: inline-block;
}

.atom-link {
align-items: center;
cursor: pointer;
display: flex;
font: var(--text-link-medium);
gap: var(--spacing-xxsmall);
letter-spacing: var(--text-link-medium-letter);
text-decoration: underline;

&[color='primary'] {
color: var(--color-brand-primary-regular);
}

&[color='secondary'] {
color: var(--color-brand-secondary-regular)
}

&__button {
background: none;
border: none;
padding: 0;
position: relative;
text-decoration: none;
}
}
74 changes: 74 additions & 0 deletions packages/core/src/components/link/link.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { newSpecPage } from '@stencil/core/testing'

import { AtomLink } from './link'

describe('atom-link', () => {
it('should render a span element with secondary color - default mode', async () => {
const page = await newSpecPage({
components: [AtomLink],
html: `<atom-link>styled link</atom-link>`,
})

await page.waitForChanges()

expect(page.root).toEqualHtml(`
<atom-link>
<mock:shadow-root>
<span class="atom-link" color="primary">
<slot></slot>
</span>
</mock:shadow-root>
styled link
</atom-link>
`)
})

it('should render a span element with secondary color', async () => {
const page = await newSpecPage({
components: [AtomLink],
html: `<atom-link color="secondary">styled link</atom-link>`,
})

await page.waitForChanges()

expect(page.root).toEqualHtml(`
<atom-link color="secondary">
<mock:shadow-root>
<span class="atom-link" color="secondary">
<slot></slot>
</span>
</mock:shadow-root>
styled link
</atom-link>
`)
})

it('should render a clickable button element with secondary color ', async () => {
const page = await newSpecPage({
components: [AtomLink],
html: `<atom-link color="secondary" type="button">styled link</atom-link>`,
})

await page.waitForChanges()

expect(page.root).toEqualHtml(`
<atom-link color="secondary" type="button">
<mock:shadow-root>
<button class="atom-link__button">
<span class="atom-link" color="secondary">
<slot></slot>
</span>
</button>
</mock:shadow-root>
styled link
</atom-link>
`)

const buttonEl = page.root?.shadowRoot?.querySelector('button')
const clickEventSpy = jest.spyOn(page.rootInstance.click, 'emit')

buttonEl?.click()

expect(clickEventSpy).toHaveBeenCalled()
})
})
41 changes: 41 additions & 0 deletions packages/core/src/components/link/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Component, Event, EventEmitter, Host, Prop, h } from '@stencil/core'

@Component({
tag: 'atom-link',
styleUrl: 'link.scss',
shadow: true,
})
export class AtomLink {
@Prop() color: 'primary' | 'secondary' = 'primary'
@Prop() type: 'anchor' | 'button' = 'anchor'

@Event() click: EventEmitter

private handleClick = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()

return this.click.emit(event)
}

render() {
return (
<Host>
{this.type === 'anchor' ? (
<span class='atom-link' color={this.color}>
<slot />
</span>
) : (
<button
class='atom-link__button'
onClick={this.handleClick.bind(this)}
>
<span class='atom-link' color={this.color}>
<slot />
</span>
</button>
)}
</Host>
)
}
}
47 changes: 47 additions & 0 deletions packages/core/src/components/link/stories/link.args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const LinkStoryArgs = {
decorators: [],
parameters: {
actions: {
handles: [],
},
docs: {
description: {
component:
'atom-link components are link children styled components. They are used to navigate to different pages (when used inside router components, such as router-link(Vue) and Link(Next)) or used to trigger user actions.',
},
},
},
argTypes: {
color: {
control: 'select',
options: ['primary', 'secondary'],
defaultValue: { summary: 'primary' },
description: 'The link color.',
},
type: {
control: 'select',
options: ['anchor', 'button'],
defaultValue: { summary: 'anchor' },
description:
'The atom-link type. Use anchor for navigation (combined with router-link or Link) and button for user actions.',
},
},
}

const LinkReactStoryArgs = JSON.parse(JSON.stringify(LinkStoryArgs))

LinkReactStoryArgs.parameters.docs.description.component =
'atom-link components are link children styled components. They are used to navigate to different pages (when used inside Link(Next)) or used to trigger user actions.<br/><br/> OBS: Link (Next) component does not render a anchor tag by default, so you need to wrap it with a tag for semantic reasons. You can create a wrapper component on your project to do this.'

LinkReactStoryArgs.argTypes.type.description =
'The atom-link type. Use anchor for navigation (combined with Link) and button for user actions.'

const LinkVueStoryArgs = JSON.parse(JSON.stringify(LinkStoryArgs))

LinkVueStoryArgs.parameters.docs.description.component =
'atom-link components are link children styled components. They are used to navigate to different pages (when used inside router-link or NuxtLink or used to trigger user actions.'

LinkVueStoryArgs.argTypes.type.description =
'The atom-link type. Use anchor for navigation (combined with router-link or NuxtLink) and button for user actions.'

export { LinkReactStoryArgs, LinkVueStoryArgs }
65 changes: 65 additions & 0 deletions packages/core/src/components/link/stories/link.core.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Meta, StoryObj } from '@storybook/web-components'
import { html } from 'lit'

import { LinkStoryArgs } from './link.args'

export default {
title: 'Components/Link',
...LinkStoryArgs,
} as Meta

const createLink = (
args,
textExample = 'It should be used inside router components'
) => {
return html`
<a href="/nice-example">
<atom-link type="${args.type}" color="${args.color}">
${textExample}
</atom-link>
</a>
`
}

export const Primary: StoryObj = {
render: (args) => createLink(args),
args: {
type: 'anchor',
color: 'primary',
},
}

export const Secondary: StoryObj = {
render: (args) => createLink(args),
args: {
...Primary.args,
color: 'secondary',
},
}

export const WithIcon: StoryObj = {
render: (args) => html`
<a href="/nice-example">
<atom-link type="${args.type}" color="${args.color}">
<span> Nice example with icon </span>
<atom-icon icon="heart" />
</atom-link>
</a>
`,
args: {
...Primary.args,
color: 'secondary',
},
}

export const Button: StoryObj = {
render: (args) => html`
<atom-link type="${args.type}" color="${args.color}">
It is a button! and can be used to trigger user actions
</atom-link>
`,
args: {
...Primary.args,
type: 'button',
},
}
Loading
Loading