From 773ed65241d9a4637b1c1d4a193e138d48c201d6 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico Date: Wed, 8 Jan 2025 15:19:04 -0500 Subject: [PATCH 1/2] New RadioGroup Co-authored-by: toan-kunaico --- apps/docs/src/routes/index.mdx | 1 + apps/docs/src/routes/menu.md | 3 +- .../src/routes/radio-group/examples/hero.tsx | 20 ++++ apps/docs/src/routes/radio-group/index.mdx | 9 ++ libs/components/src/index.ts | 3 +- .../src/pagination/pagination-root.tsx | 73 +++++++------- libs/components/src/radio-group/index.ts | 7 ++ .../src/radio-group/radio-group-context.tsx | 17 ++++ .../radio-group/radio-group-description.tsx | 33 +++++++ .../radio-group/radio-group-error-message.tsx | 31 ++++++ .../radio-group/radio-group-hidden-input.tsx | 36 +++++++ .../src/radio-group/radio-group-indicator.tsx | 32 +++++++ .../src/radio-group/radio-group-label.tsx | 23 +++++ .../src/radio-group/radio-group-root.tsx | 95 +++++++++++++++++++ .../src/radio-group/radio-group-trigger.tsx | 61 ++++++++++++ .../src/radio-group/radio-group.css | 9 ++ 16 files changed, 412 insertions(+), 41 deletions(-) create mode 100644 apps/docs/src/routes/radio-group/examples/hero.tsx create mode 100644 apps/docs/src/routes/radio-group/index.mdx create mode 100644 libs/components/src/radio-group/index.ts create mode 100644 libs/components/src/radio-group/radio-group-context.tsx create mode 100644 libs/components/src/radio-group/radio-group-description.tsx create mode 100644 libs/components/src/radio-group/radio-group-error-message.tsx create mode 100644 libs/components/src/radio-group/radio-group-hidden-input.tsx create mode 100644 libs/components/src/radio-group/radio-group-indicator.tsx create mode 100644 libs/components/src/radio-group/radio-group-label.tsx create mode 100644 libs/components/src/radio-group/radio-group-root.tsx create mode 100644 libs/components/src/radio-group/radio-group-trigger.tsx create mode 100644 libs/components/src/radio-group/radio-group.css diff --git a/apps/docs/src/routes/index.mdx b/apps/docs/src/routes/index.mdx index f2bad955..f9df65c9 100644 --- a/apps/docs/src/routes/index.mdx +++ b/apps/docs/src/routes/index.mdx @@ -11,3 +11,4 @@ Here is some MDX - [Pagination](pagination) - [OTP](otp) - [Scroll Area](scroll-area) +- [Radio Group](radio-group) diff --git a/apps/docs/src/routes/menu.md b/apps/docs/src/routes/menu.md index 086d1f17..545a3497 100644 --- a/apps/docs/src/routes/menu.md +++ b/apps/docs/src/routes/menu.md @@ -7,8 +7,9 @@ - [Pagination](/pagination) - [OTP](/otp) - [Scroll Area](/scroll-area) +- [Radio Group](/radio-group) ## Styled - [Feed](/feed) -- [Avatar](/avatar) \ No newline at end of file +- [Avatar](/avatar) diff --git a/apps/docs/src/routes/radio-group/examples/hero.tsx b/apps/docs/src/routes/radio-group/examples/hero.tsx new file mode 100644 index 00000000..a64c9495 --- /dev/null +++ b/apps/docs/src/routes/radio-group/examples/hero.tsx @@ -0,0 +1,20 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { RadioGroup } from '@kunai-consulting/qwik-components'; + +export default component$(() => { + useStyles$(styles); + + return ( + + + + + + + + ); +}); + +// example styles +import styles from './radio-group.css?inline'; +import { LuCheck } from '@qwikest/icons/lucide'; diff --git a/apps/docs/src/routes/radio-group/index.mdx b/apps/docs/src/routes/radio-group/index.mdx new file mode 100644 index 00000000..476ac231 --- /dev/null +++ b/apps/docs/src/routes/radio-group/index.mdx @@ -0,0 +1,9 @@ +--- +title: Qwik Design System | Radio Group +--- + +# Radio Group + +A button that can be toggled between two states. + + diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 377f2539..7c503970 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,5 +1,6 @@ export * as Otp from './otp'; export * as Checkbox from './checkbox'; export * as Checklist from './checklist'; -export * as Pagination from "./pagination"; +export * as Pagination from './pagination'; export * as ScrollArea from './scroll-area'; +export * as RadioGroup from './radio-group'; diff --git a/libs/components/src/pagination/pagination-root.tsx b/libs/components/src/pagination/pagination-root.tsx index a23764bd..b7bca03f 100644 --- a/libs/components/src/pagination/pagination-root.tsx +++ b/libs/components/src/pagination/pagination-root.tsx @@ -3,59 +3,56 @@ import { type QRL, Slot, component$, - JSXNode, - JSXChildren, + type JSXNode, + type JSXChildren, useContextProvider, useSignal, useTask$, type Signal, useComputed$, useId, - $ -} from "@builder.io/qwik"; + $, +} from '@builder.io/qwik'; +// import { +// processChildren, +// findComponent, +// } from "@kunai-consulting/qwik-hooks"; +import { processChildren, findComponent } from '../../utils/inline-component'; +import { PaginationPage } from './pagination-page'; import { - processChildren, - findComponent, -} from "@kunai-consulting/qwik-hooks"; -import { PaginationPage } from "./pagination-page"; -import { type PaginationContext, paginationContextId } from "./pagination-context"; -import { useBoundSignal } from "../../utils/bound-signal"; -import { getPaginationItems } from "./utils"; + type PaginationContext, + paginationContextId, +} from './pagination-context'; +import { useBoundSignal } from '../../utils/bound-signal'; +import { getPaginationItems } from './utils'; -export type PaginationRootProps = PropsOf<"div"> & { +export type PaginationRootProps = PropsOf<'div'> & { totalPages: number; currentPage?: number; - "bind:page"?: Signal; + 'bind:page'?: Signal; onPageChange$?: QRL<(page: number) => void>; disabled?: boolean; - pages: any[]; + pages: JSXNode[]; ellipsis?: JSXChildren; siblingCount?: number; }; -export const PaginationRoot = - (props: PaginationRootProps) => { - let currPageIndex = 0; +export const PaginationRoot = (props: PaginationRootProps) => { + let currPageIndex = 0; - findComponent(PaginationPage, (pageProps) => { - pageProps._index = currPageIndex; - currPageIndex++; - }); + findComponent(PaginationPage, (pageProps) => { + pageProps._index = currPageIndex; + currPageIndex++; + }); - processChildren(props.children); + processChildren(props.children); - return ( - - {props.children} - - ) - }; + return {props.children}; +}; export const PaginationBase = component$((props: PaginationRootProps) => { const { - "bind:page": givenPageSig, + 'bind:page': givenPageSig, totalPages, onPageChange$, currentPage, @@ -69,8 +66,10 @@ export const PaginationBase = component$((props: PaginationRootProps) => { const isDisabledSig = useComputed$(() => disabled); const selectedPageSig = useBoundSignal(givenPageSig, currentPage || 1); const focusedIndexSig = useSignal(null); - const ellipsisSig = useComputed$(() => getPaginationItems(totalPages,selectedPageSig.value,siblingCount || 1)); - const pagesSig = useSignal(pages) + const ellipsisSig = useComputed$(() => + getPaginationItems(totalPages, selectedPageSig.value, siblingCount || 1) + ); + const pagesSig = useSignal(pages); const context: PaginationContext = { isDisabledSig, @@ -101,18 +100,14 @@ export const PaginationBase = component$((props: PaginationRootProps) => { isInitialLoadSig.value = false; }); - - return (
); }); - - diff --git a/libs/components/src/radio-group/index.ts b/libs/components/src/radio-group/index.ts new file mode 100644 index 00000000..0ba2edf9 --- /dev/null +++ b/libs/components/src/radio-group/index.ts @@ -0,0 +1,7 @@ +export { RadioGroupRoot as Root } from './radio-group-root'; +export { RadioGroupIndicator as Indicator } from './radio-group-indicator'; +export { RadioGroupTrigger as Trigger } from './radio-group-trigger'; +export { RadioGroupLabel as Label } from './radio-group-label'; +export { RadioGroupDescription as Description } from './radio-group-description'; +export { RadioGroupHiddenNativeInput as HiddenNativeInput } from './radio-group-hidden-input'; +export { RadioGroupErrorMessage as ErrorMessage } from './radio-group-error-message'; diff --git a/libs/components/src/radio-group/radio-group-context.tsx b/libs/components/src/radio-group/radio-group-context.tsx new file mode 100644 index 00000000..5cf38b98 --- /dev/null +++ b/libs/components/src/radio-group/radio-group-context.tsx @@ -0,0 +1,17 @@ +import { createContextId, type Signal } from '@builder.io/qwik'; + +export const radioGroupContextId = createContextId( + 'qds-radio-group-context' +); + +export type RadioGroupContext = { + isCheckedSig: Signal; + isDisabledSig: Signal; + isErrorSig: Signal; + localId: string; + isDescription: boolean | undefined; + name: string | undefined; + required: boolean | undefined; + value: string | undefined; + triggerRef: Signal; +}; diff --git a/libs/components/src/radio-group/radio-group-description.tsx b/libs/components/src/radio-group/radio-group-description.tsx new file mode 100644 index 00000000..8043e08f --- /dev/null +++ b/libs/components/src/radio-group/radio-group-description.tsx @@ -0,0 +1,33 @@ +import { + component$, + type PropsOf, + Slot, + sync$, + useContext, + useOnWindow, + useTask$, +} from '@builder.io/qwik'; +import { radioGroupContextId } from './radio-group-context'; + +type RadioGroupDescriptionProps = PropsOf<'div'>; + +export const RadioGroupDescription = component$( + (props: RadioGroupDescriptionProps) => { + const context = useContext(radioGroupContextId); + const descriptionId = `${context.localId}-description`; + + useTask$(() => { + if (!context.isDescription) { + console.warn( + 'Qwik Design System Warning: No description prop provided to the Radio Group Root component.' + ); + } + }); + + return ( +
+ +
+ ); + } +); diff --git a/libs/components/src/radio-group/radio-group-error-message.tsx b/libs/components/src/radio-group/radio-group-error-message.tsx new file mode 100644 index 00000000..d61c7abf --- /dev/null +++ b/libs/components/src/radio-group/radio-group-error-message.tsx @@ -0,0 +1,31 @@ +import { + component$, + Slot, + useContext, + useTask$, + type PropsOf, +} from '@builder.io/qwik'; +import { radioGroupContextId } from './radio-group-context'; + +type RadioGroupErrorMessageProps = PropsOf<'div'>; + +export const RadioGroupErrorMessage = component$( + (props: RadioGroupErrorMessageProps) => { + const context = useContext(radioGroupContextId); + const errorId = `${context.localId}-error`; + + useTask$(({ cleanup }) => { + context.isErrorSig.value = true; + + cleanup(() => { + context.isErrorSig.value = false; + }); + }); + + return ( +
+ +
+ ); + } +); diff --git a/libs/components/src/radio-group/radio-group-hidden-input.tsx b/libs/components/src/radio-group/radio-group-hidden-input.tsx new file mode 100644 index 00000000..f6c24dfb --- /dev/null +++ b/libs/components/src/radio-group/radio-group-hidden-input.tsx @@ -0,0 +1,36 @@ +import { $, component$, useContext, type PropsOf } from '@builder.io/qwik'; +import { VisuallyHidden } from '../visually-hidden/visually-hidden'; +import { radioGroupContextId } from './radio-group-context'; + +type RadioGroupHiddenNativeInputProps = PropsOf<'input'>; + +export const RadioGroupHiddenNativeInput = component$( + (props: RadioGroupHiddenNativeInputProps) => { + const context = useContext(radioGroupContextId); + + const handleChange$ = $((e: InputEvent) => { + const target = e.target as HTMLInputElement; + if (target.checked === context.isCheckedSig.value) { + return; + } + + context.isCheckedSig.value = target.checked; + }); + + return ( + + + + ); + } +); diff --git a/libs/components/src/radio-group/radio-group-indicator.tsx b/libs/components/src/radio-group/radio-group-indicator.tsx new file mode 100644 index 00000000..904e6462 --- /dev/null +++ b/libs/components/src/radio-group/radio-group-indicator.tsx @@ -0,0 +1,32 @@ +import { + component$, + useContext, + type PropsOf, + Slot, + useTask$, + useStyles$, +} from '@builder.io/qwik'; +import { RadioGroupContext, radioGroupContextId } from './radio-group-context'; +import './radio-groupcss'; +import styles from './radio-group.css?inline'; + +export type RadioGroupIndicatorProps = PropsOf<'span'>; + +export const RadioGroupIndicator = component$( + (props) => { + useStyles$(styles); + const context = useContext(radioGroupContextId); + + return ( + + ); + } +); diff --git a/libs/components/src/radio-group/radio-group-label.tsx b/libs/components/src/radio-group/radio-group-label.tsx new file mode 100644 index 00000000..bc10e589 --- /dev/null +++ b/libs/components/src/radio-group/radio-group-label.tsx @@ -0,0 +1,23 @@ +import { + $, + component$, + type PropsOf, + Slot, + sync$, + useContext, +} from '@builder.io/qwik'; +import { radioGroupContextId } from './radio-group-context'; +import { Label } from '../label'; + +type CheckboxLabelProps = PropsOf<'label'>; + +export const RadioGroupLabel = component$((props: CheckboxLabelProps) => { + const context = useContext(radioGroupContextId); + const triggerId = `${context.localId}-trigger`; + + return ( + + ); +}); diff --git a/libs/components/src/radio-group/radio-group-root.tsx b/libs/components/src/radio-group/radio-group-root.tsx new file mode 100644 index 00000000..a4ae657d --- /dev/null +++ b/libs/components/src/radio-group/radio-group-root.tsx @@ -0,0 +1,95 @@ +import { + component$, + type PropsOf, + Slot, + type Signal, + useContextProvider, + useId, + useTask$, + useSignal, + type QRL, + useComputed$, + JSXNode, +} from '@builder.io/qwik'; +import { useBoundSignal } from '../../utils/bound-signal'; +import { + type RadioGroupContext, + radioGroupContextId, +} from './radio-group-context'; + +export type RadioGroupRootProps = { + 'bind:checked'?: Signal; + checked?: T; + onChange$?: QRL<(checked: T) => void>; + disabled?: boolean; + isDescription?: boolean; + name?: string; + required?: boolean; + value?: string; +} & PropsOf<'div'>; + +export const RadioGroupRoot = component$( + (props: RadioGroupRootProps) => { + const { + 'bind:checked': givenCheckedSig, + checked, + onClick$, + onChange$, + isDescription, + name, + required, + value, + ...rest + } = props; + + const isCheckedSig = useBoundSignal( + givenCheckedSig, + checked ?? false + ); + const isInitialLoadSig = useSignal(true); + const isDisabledSig = useComputed$(() => props.disabled); + const isErrorSig = useSignal(false); + const localId = useId(); + const triggerRef = useSignal(); + + const context: RadioGroupContext = { + isCheckedSig, + isDisabledSig, + localId, + isDescription, + name, + required, + value, + isErrorSig, + triggerRef, + }; + + useContextProvider(radioGroupContextId, context); + + useTask$(async function handleChange({ track }) { + track(() => isCheckedSig.value); + + if (isInitialLoadSig.value) { + return; + } + + await onChange$?.(isCheckedSig.value as boolean); + }); + + useTask$(() => { + isInitialLoadSig.value = false; + }); + + return ( +
+ +
+ ); + } +); diff --git a/libs/components/src/radio-group/radio-group-trigger.tsx b/libs/components/src/radio-group/radio-group-trigger.tsx new file mode 100644 index 00000000..235082e9 --- /dev/null +++ b/libs/components/src/radio-group/radio-group-trigger.tsx @@ -0,0 +1,61 @@ +import { + $, + component$, + type PropsOf, + Slot, + sync$, + useComputed$, + useContext, +} from '@builder.io/qwik'; +import { radioGroupContextId } from './radio-group-context'; + +type RadioGroupControlProps = PropsOf<'button'>; + +export const RadioGroupTrigger = component$((props: RadioGroupControlProps) => { + const context = useContext(radioGroupContextId); + const triggerId = `${context.localId}-trigger`; + const descriptionId = `${context.localId}-description`; + const errorId = `${context.localId}-error`; + + const describedByLabels = useComputed$(() => { + const labels = []; + if (context.isDescription) { + labels.push(descriptionId); + } + if (context.isErrorSig.value) { + labels.push(errorId); + } + return labels.join(' ') || undefined; + }); + + const handleClick$ = $(() => { + context.isCheckedSig.value = !context.isCheckedSig.value; + }); + + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + } + }); + + return ( + + ); +}); diff --git a/libs/components/src/radio-group/radio-group.css b/libs/components/src/radio-group/radio-group.css new file mode 100644 index 00000000..85fd9cb6 --- /dev/null +++ b/libs/components/src/radio-group/radio-group.css @@ -0,0 +1,9 @@ +@layer qds-radio-group { + [data-qds-indicator] { + user-select: none; + } + + [data-qds-indicator][data-hidden] { + display: none; + } +} From 14a0c3f731af8612b29f03acdb1057a8fc89cb83 Mon Sep 17 00:00:00 2001 From: Jay-Kunaico Date: Wed, 8 Jan 2025 16:49:12 -0500 Subject: [PATCH 2/2] updated css --- .../src/routes/pagination/auto-api/api.ts | 68 +++++++++++++++++++ .../radio-group/examples/radio-group.css | 24 +++++++ .../src/pagination/pagination-root.tsx | 2 +- .../src/radio-group/radio-group-indicator.tsx | 2 +- 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 apps/docs/src/routes/pagination/auto-api/api.ts create mode 100644 apps/docs/src/routes/radio-group/examples/radio-group.css diff --git a/apps/docs/src/routes/pagination/auto-api/api.ts b/apps/docs/src/routes/pagination/auto-api/api.ts new file mode 100644 index 00000000..53c1a785 --- /dev/null +++ b/apps/docs/src/routes/pagination/auto-api/api.ts @@ -0,0 +1,68 @@ +export const api = { + "pagination": [ + { + "Pagination Ellipsis": { + "types": [], + "inheritsFrom": "div" + } + }, + { + "Pagination Next": { + "types": [], + "inheritsFrom": "button" + } + }, + { + "Pagination Page": { + "types": [], + "dataAttributes": [ + { + "name": "data-index", + "type": "string" + }, + { + "name": "data-current", + "type": "string" + } + ] + } + }, + { + "Pagination Previous": { + "types": [], + "inheritsFrom": "button" + } + }, + { + "Pagination Root": { + "types": [], + "inheritsFrom": "div", + "dataAttributes": [ + { + "name": "data-disabled", + "type": "string | undefined" + } + ] + } + } + ], + "anatomy": [ + { + "name": "Pagination.Root" + }, + { + "name": "Pagination.Page" + }, + { + "name": "Pagination.Next" + }, + { + "name": "Pagination.Previous" + }, + { + "name": "Pagination.Ellipsis" + } + ], + "keyboardInteractions": [], + "features": [] +}; \ No newline at end of file diff --git a/apps/docs/src/routes/radio-group/examples/radio-group.css b/apps/docs/src/routes/radio-group/examples/radio-group.css new file mode 100644 index 00000000..1552892c --- /dev/null +++ b/apps/docs/src/routes/radio-group/examples/radio-group.css @@ -0,0 +1,24 @@ +.radio-group-root { + display: flex; + align-items: center; + gap: 8px; +} + +.radrio-group-trigger { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 4px; + border: 1px solid #ccc; +} + +.radio-group-indicator { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: #ccc; +} diff --git a/libs/components/src/pagination/pagination-root.tsx b/libs/components/src/pagination/pagination-root.tsx index a75350bf..8b11fcfb 100644 --- a/libs/components/src/pagination/pagination-root.tsx +++ b/libs/components/src/pagination/pagination-root.tsx @@ -28,7 +28,7 @@ export type PaginationRootProps = PropsOf<'div'> & { 'bind:page'?: Signal; onPageChange$?: QRL<(page: number) => void>; disabled?: boolean; - pages: JSXNode[]; + pages: number[]; ellipsis?: JSXChildren; siblingCount?: number; }; diff --git a/libs/components/src/radio-group/radio-group-indicator.tsx b/libs/components/src/radio-group/radio-group-indicator.tsx index 904e6462..e6937f12 100644 --- a/libs/components/src/radio-group/radio-group-indicator.tsx +++ b/libs/components/src/radio-group/radio-group-indicator.tsx @@ -7,7 +7,7 @@ import { useStyles$, } from '@builder.io/qwik'; import { RadioGroupContext, radioGroupContextId } from './radio-group-context'; -import './radio-groupcss'; +import './radio-group.css'; import styles from './radio-group.css?inline'; export type RadioGroupIndicatorProps = PropsOf<'span'>;