Skip to content

Commit

Permalink
Merge pull request #34 from meretamal/main
Browse files Browse the repository at this point in the history
Release 0.3.0
  • Loading branch information
meretamal authored May 1, 2023
2 parents d9be7a9 + 9fda599 commit 2c09ae4
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 84 deletions.
4 changes: 4 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
## Fixes 🐛

- Include here what was fixed with this version.

## Engineering 💻

- Include here what was added or changed, related to developer experience, testing, etc.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "celeste-ui",
"private": false,
"version": "0.2.2",
"version": "0.3.0",
"type": "module",
"main": "dist/index.umd.js",
"module": "dist/index.es.js",
Expand Down
69 changes: 69 additions & 0 deletions src/components/c-checkbox/c-checkbox.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import '@testing-library/jest-dom';

import { describe, expect, it } from 'vitest';
import { render, RenderOptions, screen, fireEvent } from '@testing-library/vue';
import { CCheckbox } from './c-checkbox';

const renderComponent = (props?: RenderOptions['props']) =>
render(CCheckbox, { props });

describe('CCheckbox', () => {
it('should render the component correctly', () => {
const container = renderComponent();
const input = container.baseElement.querySelector('div[role="checkbox"]');
expect(input).toBeInTheDocument();
});

it('should display the given label', () => {
renderComponent({ label: 'Remember me', id: 'remember-me' });
screen.getByLabelText('Remember me');
});

it('should disable the input', () => {
renderComponent({
label: 'Remember me',
id: 'remember-me',
disabled: true,
});
const input = screen.getByLabelText('Remember me');
expect(input).toHaveAttribute('aria-disabled', 'true');
});

it('should toggle a boolean when passing a boolean v-model', async () => {
const container = renderComponent({
modelValue: false,
label: 'Remember me',
id: 'remember-me',
});
const input = screen.getByLabelText('Remember me');
await fireEvent.click(input);
const emitted = container.emitted()['update:modelValue'] as boolean[][];
expect(emitted[0][0]).toBe(true);
});

it('should emit the given value in an array if passing an array as a v-model', async () => {
const container = renderComponent({
modelValue: [],
value: 'plane',
label: 'Plane',
id: 'transportation',
});
const input = screen.getByLabelText('Plane');
await fireEvent.click(input);
const emitted = container.emitted()['update:modelValue'] as string[][][];
expect(emitted[0][0]).toEqual(['plane']);
});

it('should remove the given value if passing an array as a v-model and it already has it', async () => {
const container = renderComponent({
modelValue: ['plane'],
value: 'plane',
label: 'Plane',
id: 'transportation',
});
const input = screen.getByLabelText('Plane');
await fireEvent.click(input);
const emitted = container.emitted()['update:modelValue'] as string[][][];
expect(emitted[0][0]).toEqual([]);
});
});
9 changes: 9 additions & 0 deletions src/components/c-checkbox/c-checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export default {
defaultValue: { summary: '"primary"' },
},
},
defaultChecked: {
control: { type: 'boolean' },
description: 'Checks the input in the initial render',
defaultValue: false,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
},
disabled: {
control: { type: 'boolean' },
description: 'Sets the disabled property of the checkbox',
Expand Down
52 changes: 25 additions & 27 deletions src/components/c-checkbox/c-checkbox.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,77 +12,75 @@ export const useCheckboxStyles = () => {
display: 'flex',
alignItems: 'center',
'&--small': {
gap: '1.5rem',
gap: '0.5rem',
},
'&--medium': {
gap: '1.75rem',
gap: '0.75rem',
},
'&--large': {
gap: '2rem',
gap: '1rem',
},
'&__input': {
position: 'relative',
width: 0,
cursor: 'pointer',
'&::before': {
boxSizing: 'border-box',
content: '""',
border: '2px solid #bdbdbd',
position: 'absolute',
transition: 'all 0.1s ease-in-out',
height: '100%',
border: '2px solid #bdbdbd',
transition: 'all 0.1s ease-in-out',
height: '100%',
'&:focus': {
outline: 'none',
},
'&::after': {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
visibility: 'hidden',
content:
"url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='3' stroke='white' class='w-6 h-6'><path stroke-linecap='round' stroke-linejoin='round' d='M4.5 12.75l6 6 9-13.5' /></svg>\")",
"url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='3' stroke='white'><path stroke-linecap='round' stroke-linejoin='round' d='M4.5 12.75l6 6 9-13.5' /></svg>\")",
height: '100%',
},
'&:checked::after': {
'&[aria-checked="true"]::after': {
visibility: 'visible',
},
'&:disabled, &[disabled]': {
'&[aria-disabled="true"]': {
cursor: 'not-allowed',
},
},
'&--small &__input': {
height: '0.75rem',
width: '0.75rem',
borderRadius: '2px',
},
'&--medium &__input': {
height: '1rem',
width: '1rem',
borderRadius: '3px',
},
'&--large &__input': {
height: '1.25rem',
},
'&--small &__input::before': {
borderRadius: '2px',
},
'&--medium &__input::before': {
borderRadius: '3px',
},
'&--large &__input::before': {
width: '1.25rem',
borderRadius: '4px',
},
'&--small &__input::before, &--small &__input::after': {
'&--small &__input::after': {
top: 0,
width: '0.75rem',
},
'&--medium &__input::before, &--medium &__input::after': {
'&--medium &__input::after': {
width: '1rem',
},
'&--large &__input::before, &--large &__input::after': {
'&--large &__input::after': {
width: '1.25rem',
},
...(
Object.keys(theme.value.colors) as (keyof typeof theme.value.colors)[]
).reduce(
(prev, color) => ({
...prev,
[`&--${color} &__input:checked::before`]: {
[`&--${color} &__input[aria-checked="true"]`]: {
backgroundColor: theme.value.colors[color],
borderColor: theme.value.colors[color],
},
[`&--${color} &__input:focus::before`]: {
[`&--${color} &__input[aria-disabled="false"]:focus`]: {
borderColor: theme.value.colors[color],
boxShadow: `0 0 0 3px ${mix(0.5, '#fff', theme.value.colors[color])}`,
},
Expand Down
79 changes: 53 additions & 26 deletions src/components/c-checkbox/c-checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { computed, defineComponent, PropType } from 'vue';
import { computed, defineComponent, PropType, ref } from 'vue';
import { celeste } from '@/celeste';
import { useCheckboxStyles } from './c-checkbox.styles';

Expand Down Expand Up @@ -30,14 +30,27 @@ export const CCheckbox = defineComponent({
disabled: {
type: Boolean,
},
defaultChecked: {
type: Boolean,
},
id: {
type: String,
default: undefined,
},
},
emits: ['update:modelValue'],
emits: ['update:modelValue', 'input', 'change'],
setup(props, { emit }) {
const isChecked = computed(() =>
Array.isArray(props.modelValue)
? props.modelValue.includes(props.value)
: props.modelValue,
);
const internalChecked = ref(props.defaultChecked);

const isChecked = computed(() => {
if (Array.isArray(props.modelValue)) {
return props.modelValue.length > 0
? props.modelValue.includes(props.value)
: internalChecked.value;
}
return props.modelValue;
});

const baseClass = useCheckboxStyles();
const containerClasses = computed(() => [
baseClass.value,
Expand All @@ -46,42 +59,56 @@ export const CCheckbox = defineComponent({
]);
const inputClasses = computed(() => [`${baseClass.value}__input`]);

const handleInput = (event: Event) => {
const handleInput = () => {
const newCheckedState = !isChecked.value;
if (Array.isArray(props.modelValue)) {
const index = props.modelValue.indexOf(props.value);
if (
(event.currentTarget as HTMLInputElement)?.checked &&
index === -1
) {
if (newCheckedState && index === -1) {
emit('update:modelValue', [...props.modelValue, props.value]);
} else if (
!(event.currentTarget as HTMLInputElement)?.checked &&
index !== -1
) {
} else if (!newCheckedState && index !== -1) {
emit('update:modelValue', [
...props.modelValue.slice(0, index),
...props.modelValue.slice(index + 1),
]);
}
} else {
emit(
'update:modelValue',
(event.currentTarget as HTMLInputElement)?.checked,
);
emit('update:modelValue', newCheckedState);
}
emit('input', newCheckedState);
emit('change', newCheckedState);
internalChecked.value = newCheckedState;
};

const handleClick = (event: Event) => {
if (props.disabled) {
event.stopPropagation();
return;
}
handleInput();
};

const handleKeyPress = (event: KeyboardEvent) => {
if (props.disabled || !(event.key === 'Enter' || event.key === ' ')) {
event.stopPropagation();
return;
}
handleInput();
};

return () => (
<celeste.div class={containerClasses.value}>
<input
<div
class={inputClasses.value}
type="checkbox"
checked={isChecked.value}
onInput={handleInput}
disabled={props.disabled}
role="checkbox"
onClick={handleClick}
onKeypress={handleKeyPress}
tabindex={props.disabled ? -1 : 0}
aria-disabled={props.disabled}
aria-checked={isChecked.value}
aria-labelledby={props.id}
/>
{props.label && (
<celeste.label class={`${baseClass.value}__label`}>
<celeste.label class={`${baseClass.value}__label`} id={props.id}>
{props.label}
</celeste.label>
)}
Expand Down
Loading

0 comments on commit 2c09ae4

Please sign in to comment.