Skip to content
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
49 changes: 49 additions & 0 deletions .ladle/adapters/PlainComponentAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type { PaginationControlProps } from '@/components/Common/PaginationContr
import type { TextProps } from '@/components/Common/UI/Text/TextTypes'
import type { CalendarPreviewProps } from '@/components/Common/UI/CalendarPreview/CalendarPreviewTypes'
import type { DialogProps } from '@/components/Common/UI/Dialog/DialogTypes'
import type { LoadingSpinnerProps } from '@/components/Common/UI/LoadingSpinner/LoadingSpinnerTypes'

export const PlainComponentAdapter: ComponentsContextType = {
Alert: ({ label, children, status = 'info', icon }: AlertProps) => {
Expand Down Expand Up @@ -1292,4 +1293,52 @@ export const PlainComponentAdapter: ComponentsContextType = {
</dialog>
)
},

LoadingSpinner: ({ size = 'lg', style = 'block', className, ...props }: LoadingSpinnerProps) => {
const spinnerSize = size === 'lg' ? 80 : 40

return (
<div
{...props}
className={className}
role="status"
aria-label={props['aria-label'] || 'Loading'}
style={{
display: style === 'inline' ? 'inline-flex' : 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<svg
width={spinnerSize}
height={spinnerSize}
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
style={{
animation: 'spin 1s linear infinite',
color: 'currentColor',
}}
>
<path
d="M47.3006 79.0698C41.5319 80.1477 35.5961 79.9349 29.9194 78.4465C24.2428 76.9581 18.9661 74.2311 14.4685 70.4613C9.97083 66.6916 6.36377 61.9726 3.90636 56.6434C1.44895 51.3141 0.202168 45.5068 0.255551 39.6385L6.46944 39.695C6.4244 44.6458 7.47625 49.5452 9.54946 54.0412C11.6227 58.5373 14.6658 62.5184 18.4602 65.6988C22.2547 68.8792 26.7063 71.1798 31.4955 72.4355C36.2846 73.6912 41.2924 73.8708 46.1592 72.9614L47.3006 79.0698Z"
fill="currentColor"
/>
</svg>
<style>
{`
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`}
</style>
</div>
)
},
}
3 changes: 3 additions & 0 deletions src/assets/icons/spinner_large.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/Common/Loading/Loading.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
.skeletonBox {
height: 200px;
width: 100%;
padding: toReam(10);
display: flex;
align-items: center;
justify-content: center;
}

@keyframes pulse {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Common/Loading/Loading.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export default {
export const Default = () => {
return <Loading />
}

export const WithChildren = () => {
return <Loading>Child content</Loading>
}
9 changes: 7 additions & 2 deletions src/components/Common/Loading/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { ReactNode } from 'react'
import styles from './Loading.module.scss'
import { FadeIn } from '@/components/Common/FadeIn/FadeIn'

export const Loading = () => {
export interface LoadingProps {
children?: ReactNode
}

export const Loading = ({ children }: LoadingProps) => {
const { t } = useTranslation('common')
return (
<FadeIn>
Expand All @@ -13,7 +18,7 @@ export const Loading = () => {
aria-live="polite"
aria-busy
>
<div className={cn(styles.skeleton, styles.skeletonBox)}></div>
<div className={cn(styles.skeleton, styles.skeletonBox)}>{children}</div>
</section>
</FadeIn>
)
Expand Down
40 changes: 40 additions & 0 deletions src/components/Common/UI/LoadingSpinner/LoadingSpinner.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.loadingSpinner {
align-items: center;
justify-content: center;

.spinnerIcon {
animation: spin 1s linear infinite;
color: var(--g-colorPrimary);
}

&[data-style='inline'] {
display: inline-flex;
}

&[data-style='block'] {
display: flex;
}

&[data-size='lg'] {
.spinnerIcon {
width: toRem(80);
height: toRem(80);
}
}

&[data-size='sm'] {
.spinnerIcon {
width: toRem(20);
height: toRem(20);
}
}
}
41 changes: 41 additions & 0 deletions src/components/Common/UI/LoadingSpinner/LoadingSpinner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Story } from '@ladle/react'
import type { LoadingSpinnerProps } from './LoadingSpinnerTypes'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'

const LoadingSpinnerWrapper = (props: LoadingSpinnerProps) => {
const Components = useComponentContext()
return <Components.LoadingSpinner {...props} />
}

export default {
title: 'UI/Components/LoadingSpinner',
component: LoadingSpinnerWrapper,
}

export const Large: Story<LoadingSpinnerProps> = args => <LoadingSpinnerWrapper {...args} />
Large.args = {
size: 'lg',
style: 'block',
}

export const Small: Story<LoadingSpinnerProps> = args => <LoadingSpinnerWrapper {...args} />
Small.args = {
size: 'sm',
style: 'block',
}

export const Inline: Story<LoadingSpinnerProps> = args => (
<div>
Loading <LoadingSpinnerWrapper {...args} /> please wait...
</div>
)
Inline.args = {
size: 'sm',
style: 'inline',
}

export const Block: Story<LoadingSpinnerProps> = args => <LoadingSpinnerWrapper {...args} />
Block.args = {
size: 'lg',
style: 'block',
}
34 changes: 34 additions & 0 deletions src/components/Common/UI/LoadingSpinner/LoadingSpinner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it } from 'vitest'
import { LoadingSpinner } from './LoadingSpinner'
import { renderWithProviders } from '@/test-utils/renderWithProviders'

describe('LoadingSpinner', () => {
describe('Accessibility', () => {
const testCases = [
{
name: 'large spinner',
props: { size: 'lg' as const },
},
{
name: 'small spinner',
props: { size: 'sm' as const },
},
{
name: 'inline spinner',
props: { style: 'inline' as const },
},
{
name: 'block spinner',
props: { style: 'block' as const },
},
]

it.each(testCases)(
'should not have any accessibility violations - $name',
async ({ props }) => {
const { container } = renderWithProviders(<LoadingSpinner {...props} />)
await expectNoAxeViolations(container)
},
)
})
})
25 changes: 25 additions & 0 deletions src/components/Common/UI/LoadingSpinner/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type React from 'react'
import classnames from 'classnames'
import styles from './LoadingSpinner.module.scss'
import type { LoadingSpinnerProps } from './LoadingSpinnerTypes'
import { LoadingSpinnerDefaults } from './LoadingSpinnerTypes'
import { applyMissingDefaults } from '@/helpers/applyMissingDefaults'
import SpinnerIcon from '@/assets/icons/spinner_large.svg?react'

export const LoadingSpinner: React.FC<LoadingSpinnerProps> = rawProps => {
const resolvedProps = applyMissingDefaults(rawProps, LoadingSpinnerDefaults)
const { className, size, style, ...otherProps } = resolvedProps

return (
<div
{...otherProps}
className={classnames(styles.loadingSpinner, className)}
data-size={size}
data-style={style}
role="status"
aria-label={otherProps['aria-label'] || 'Loading'}
>
<SpinnerIcon className={styles.spinnerIcon} aria-hidden="true" />
</div>
)
}
21 changes: 21 additions & 0 deletions src/components/Common/UI/LoadingSpinner/LoadingSpinnerTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { HTMLAttributes } from 'react'

export interface LoadingSpinnerProps
extends Pick<HTMLAttributes<HTMLDivElement>, 'className' | 'id' | 'aria-label'> {
/**
* Size of the spinner
*/
size?: 'lg' | 'sm'
/**
* Display style of the spinner
*/
style?: 'inline' | 'block'
}

/**
* Default prop values for LoadingSpinner component.
*/
export const LoadingSpinnerDefaults = {
size: 'lg',
style: 'block',
} as const satisfies Partial<LoadingSpinnerProps>
3 changes: 3 additions & 0 deletions src/components/Common/UI/LoadingSpinner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { LoadingSpinner } from './LoadingSpinner'
export type { LoadingSpinnerProps } from './LoadingSpinnerTypes'
export { LoadingSpinnerDefaults } from './LoadingSpinnerTypes'
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ export const PayrollConfigurationStory = () => {
endDate: '2025-08-13',
payScheduleUuid: 'test-pay-schedule-uuid',
}}
onBack={action('on_back')}
onCalculatePayroll={action('on_calculate')}
onEdit={action('on_edit')}
/>
Expand Down
Loading