Skip to content

Commit

Permalink
feat(atomic): add radio-button function for lit components (#4864)
Browse files Browse the repository at this point in the history
Add radio-button element for lit components
https://coveord.atlassian.net/browse/KIT-3834

---------

Co-authored-by: Frederic Beaudoin <[email protected]>
  • Loading branch information
y-lakhdar and fbeaudoincoveo authored Jan 22, 2025
1 parent e84d61b commit c813a3f
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {once, randomID} from '../../../../utils/utils';
import {Button} from '../../button';
import {FieldsetGroup} from '../../fieldset-group';
import {IconButton} from '../../iconButton';
import {RadioButton} from '../../radio-button';
import {RadioButton} from '../../stencil-radio-button';

/**
* @internal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {FunctionalComponent, h} from '@stencil/core';
import {RadioButton} from '../radio-button';
import {RadioButton} from '../stencil-radio-button';

interface ChoicesProps {
label: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/atomic/src/components/common/pager/pager-buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {FunctionalComponent, h} from '@stencil/core';
import {i18n} from 'i18next';
import {Button, ButtonProps} from '../button';
import {RadioButton, RadioButtonProps} from '../radio-button';
import {RadioButton, StencilRadioButtonProps} from '../stencil-radio-button';

export interface PagerNavigationButtonProps
extends Omit<ButtonProps, 'style' | 'part' | 'class'> {
Expand All @@ -11,7 +11,7 @@ export interface PagerNavigationButtonProps

export interface PagerPageButtonProps
extends Omit<
RadioButtonProps,
StencilRadioButtonProps,
'part' | 'style' | 'checked' | 'ariaCurrent' | 'key' | 'class'
> {
page: number;
Expand Down
129 changes: 129 additions & 0 deletions packages/atomic/src/components/common/radio-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {html, TemplateResult} from 'lit';
import {ifDefined} from 'lit-html/directives/if-defined.js';
import {classMap} from 'lit/directives/class-map.js';
import {ref, RefOrCallback} from 'lit/directives/ref.js';
import {createRipple} from '../../utils/ripple';
import {
ButtonStyle,
getClassNameForButtonStyle,
getRippleColorForButtonStyle,
} from './button-style';

export interface RadioButtonProps {
groupName: string;
selectWhenFocused?: boolean;
onChecked?(): void;
style?: ButtonStyle;
key?: string | number;
checked?: boolean;
class?: string;
text?: string;
part?: string;
ariaLabel?: string;
ariaCurrent?:
| 'page'
| 'step'
| 'location'
| 'date'
| 'time'
| 'true'
| 'false';
ref?: RefOrCallback;
}

// TODO: KIT-3822: add unit tests to this function
export const radioButton = (props: RadioButtonProps): TemplateResult => {
const classNames = {
'btn-radio': true,
selected: Boolean(props.checked),
...(props.class && {[props.class]: true}),
...(props.style && {[getClassNameForButtonStyle(props.style)]: true}),
};

const onMouseDown = (e: MouseEvent) => {
if (props.style) {
const rippleColor = getRippleColorForButtonStyle(props.style);
createRipple(e, {color: rippleColor});
}
};

const handleKeyDown = (event: KeyboardEvent) => {
if (props.selectWhenFocused !== false) {
return;
}
const {key} = event;
const radioGroup = (event.currentTarget as HTMLElement).parentNode;

if (!radioGroup || !isArrowKey(key)) {
return;
}

event.preventDefault();

const buttons = getRadioButtons(radioGroup);
const currentIndex = getCurrentIndex(
buttons,
event.currentTarget as HTMLInputElement
);
const newIndex = getNewIndex(key, currentIndex, buttons.length);

if (buttons[newIndex]) {
buttons[newIndex].focus();
}
};

const isArrowKey = (key: string) => {
return ['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp'].includes(key);
};

const getRadioButtons = (radioGroup: ParentNode) => {
return Array.from(
radioGroup.querySelectorAll('[type="radio"]')
) as HTMLInputElement[];
};

const getCurrentIndex = (
buttons: HTMLInputElement[],
currentButton: HTMLInputElement
) => {
return buttons.findIndex((button) => button === currentButton);
};

const getNewIndex = (key: string, currentIndex: number, length: number) => {
switch (key) {
case 'ArrowLeft':
case 'ArrowUp':
return (currentIndex - 1 + length) % length;
case 'ArrowRight':
case 'ArrowDown':
return (currentIndex + 1) % length;
default:
return currentIndex;
}
};

const onChange = (e: Event) => {
const input = e.currentTarget as HTMLInputElement;
if (input.checked && props.onChecked) {
props.onChecked();
}
};

return html`
<input
type="radio"
name=${props.groupName}
class=${classMap(classNames)}
value=${ifDefined(props.text)}
part=${ifDefined(props.part)}
aria-label=${ifDefined(props.ariaLabel ?? props.text)}
aria-current=${ifDefined(props.ariaCurrent)}
?checked=${Boolean(props.checked)}
.key=${props.key}
@change=${onChange}
@keydown=${handleKeyDown}
@mousedown=${onMouseDown}
${ref(props.ref)}
/>
`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {i18n} from 'i18next';
import Checkmark from '../../../images/checkmark.svg';
import Cross from '../../../images/cross.svg';
import {Button} from '../button';
import {RadioButton} from '../radio-button';
import {RadioButton} from '../stencil-radio-button';

interface SmartSnippetFeedbackBannerProps {
i18n: i18n;
Expand Down
55 changes: 55 additions & 0 deletions packages/atomic/src/components/common/stencil-button-style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export type ButtonStyle =
| 'primary'
| 'outline-primary'
| 'outline-neutral'
| 'outline-error'
| 'outline-bg-neutral'
| 'outline-bg-error'
| 'text-primary'
| 'text-neutral'
| 'text-transparent'
| 'square-neutral';

/**
* @deprecated Should only be used for Stencil components; for Lit components, use the button-style.ts
* This file is required to be in a tsx file to be able to use it in Stencil components.
*/
export function getClassNameForButtonStyle(buttonStyle: ButtonStyle) {
switch (buttonStyle) {
case 'primary':
return 'btn-primary';
case 'outline-primary':
return 'btn-outline-primary';
case 'outline-neutral':
return 'btn-outline-neutral';
case 'outline-error':
return 'btn-outline-error';
case 'outline-bg-neutral':
return 'btn-outline-bg-neutral';
case 'outline-bg-error':
return 'btn-outline-bg-error';
case 'text-primary':
return 'btn-text-primary';
case 'text-neutral':
return 'btn-text-neutral';
case 'text-transparent':
return 'btn-text-transparent';
case 'square-neutral':
return 'btn-square-neutral';
}
}

/**
* @deprecated Should only be used for Stencil components; for Lit components, use the button-style.ts
* This file is required to be in a tsx file to be able to use it in Stencil components.
*/
export function getRippleColorForButtonStyle(buttonStyle: ButtonStyle) {
switch (buttonStyle) {
case 'primary':
return 'primary';
case 'text-transparent':
return 'neutral-light';
default:
return 'neutral';
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
import {FunctionalComponent, h} from '@stencil/core';
import {JSXBase} from '@stencil/core/internal';
import {createRipple} from '../../utils/ripple';
import {RadioButtonProps} from './radio-button';
import {
ButtonStyle,
getClassNameForButtonStyle,
getRippleColorForButtonStyle,
} from './button-style';
} from './stencil-button-style';

export interface RadioButtonProps {
groupName: string;
selectWhenFocused?: boolean;
onChecked?(): void;
style?: ButtonStyle;
key?: string | number;
checked?: boolean;
class?: string;
text?: string;
part?: string;
ariaLabel?: string;
ariaCurrent?: string;
/**
* @deprecated Should only be used for Stencil components; for Lit components, use the RadioButtonProps from radio-button.ts instead.
*/
export interface StencilRadioButtonProps extends Omit<RadioButtonProps, 'ref'> {
ref?(element?: HTMLInputElement): void;
}

export const RadioButton: FunctionalComponent<RadioButtonProps> = (props) => {
/**
* @deprecated Should only be used for Stencil components; for Lit components, use the radioButton function instead.
*/
export const RadioButton: FunctionalComponent<StencilRadioButtonProps> = (
props
) => {
const classNames = ['btn-radio'];
let onMouseDown:
| JSXBase.DOMAttributes<HTMLInputElement>['onMouseDown']
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {FunctionalComponent, h} from '@stencil/core';
import {i18n} from 'i18next';
import {encodeForDomAttribute} from '../../../utils/string-utils';
import {getClassNameForButtonStyle} from '../button-style';
import {getClassNameForButtonStyle} from '../stencil-button-style';
import {SearchBoxSuggestionElement} from './suggestions-common';

export const getPartialInstantItemElement = (
Expand Down

0 comments on commit c813a3f

Please sign in to comment.