├── lib ├── Button │ ├── slots.ts │ └── index.ts ├── Meter │ ├── slots.ts │ ├── index.ts │ └── Meter.tsx ├── Popper │ ├── slots.ts │ ├── index.ts │ └── types.ts ├── Radio │ ├── slots.ts │ └── index.ts ├── Switch │ ├── slots.ts │ └── index.ts ├── Toggle │ ├── slots.ts │ └── index.ts ├── Checkbox │ ├── slots.ts │ └── index.ts ├── CheckGroup │ ├── slots.ts │ ├── index.ts │ └── context.ts ├── ProgressBar │ ├── slots.ts │ └── index.ts ├── RadioGroup │ ├── slots.ts │ ├── index.ts │ └── context.ts ├── ToggleGroup │ ├── slots.ts │ ├── index.ts │ └── context.ts ├── As │ ├── index.ts │ ├── As.tsx │ ├── AsClone.tsx │ └── utils.ts ├── Portal │ ├── index.ts │ ├── utils.ts │ └── Portal.tsx ├── internals │ ├── FocusTrap │ │ ├── index.ts │ │ └── FocusTrap.tsx │ ├── FocusRedirect │ │ └── index.ts │ ├── prefix-message.ts │ ├── SystemError.ts │ ├── resolve-prop-with-render-context.ts │ ├── keys.ts │ ├── index.ts │ ├── styles.ts │ ├── logger.ts │ ├── get-label-info.ts │ └── use-jump-to-char.ts ├── Menu │ ├── components │ │ ├── RadioGroup │ │ │ ├── index.ts │ │ │ ├── context.ts │ │ │ └── RadioGroup.tsx │ │ ├── Item │ │ │ ├── index.ts │ │ │ └── context.ts │ │ ├── index.ts │ │ ├── SeparatorItem.tsx │ │ ├── Group.tsx │ │ └── SubMenu.tsx │ ├── index.ts │ ├── constants.ts │ ├── slots.ts │ ├── context.ts │ └── BaseMenu.tsx ├── TabGroup │ ├── index.ts │ ├── slots.ts │ ├── components │ │ ├── index.ts │ │ ├── List.tsx │ │ └── Panel.tsx │ ├── context.ts │ └── TabGroup.tsx ├── Breadcrumb │ ├── index.ts │ ├── slots.ts │ ├── components │ │ ├── index.ts │ │ ├── Item.tsx │ │ ├── SeparatorItem.tsx │ │ └── List.tsx │ ├── Breadcrumb.test.tsx │ └── Breadcrumb.tsx ├── PreserveAspectRatio │ ├── index.ts │ ├── slots.ts │ └── PreserveAspectRatio.tsx ├── TreeView │ ├── components │ │ ├── Item │ │ │ ├── index.ts │ │ │ └── context.ts │ │ ├── index.ts │ │ └── SubTree.tsx │ ├── index.ts │ ├── slots.ts │ ├── contexts.ts │ └── utils.ts ├── Toast │ ├── slots.ts │ ├── components │ │ ├── index.ts │ │ ├── Action.tsx │ │ └── Content.tsx │ ├── index.ts │ ├── context.ts │ └── Toast.test.tsx ├── Expandable │ ├── slots.ts │ ├── components │ │ ├── index.ts │ │ ├── Content.tsx │ │ └── Trigger.tsx │ ├── index.ts │ ├── context.ts │ └── Expandable.tsx ├── Select │ ├── components │ │ ├── Controller │ │ │ └── index.ts │ │ ├── List │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── EmptyStatement.tsx │ │ ├── Trigger.tsx │ │ └── Group.tsx │ ├── index.ts │ ├── slots.ts │ ├── context.ts │ └── utils.ts ├── Tooltip │ └── index.ts ├── PortalConfigProvider │ ├── index.ts │ ├── usePortalConfig.ts │ ├── PortalConfigProvider.tsx │ └── context.ts ├── SpinButton │ ├── slots.ts │ ├── index.ts │ ├── components │ │ ├── index.ts │ │ ├── DecrementButton.tsx │ │ └── IncrementButton.tsx │ └── context.ts ├── Dialog │ ├── index.ts │ ├── slots.ts │ ├── components │ │ ├── index.ts │ │ ├── Backdrop.tsx │ │ ├── Content.tsx │ │ ├── Title.tsx │ │ └── Description.tsx │ └── context.ts ├── utils │ ├── fork-refs.ts │ ├── use-isomorphic-layout-effect.ts │ ├── set-ref.ts │ ├── get-direction.ts │ ├── create-custom-event.ts │ ├── use-is-initial-render-complete.ts │ ├── create-virtual-element.ts │ ├── component-with-forwarded-ref.ts │ ├── use-isomorphic-value.ts │ ├── use-event-callback.ts │ ├── request-form-submit.ts │ ├── get-scrolling-state.ts │ ├── math.ts │ ├── index.ts │ ├── is.ts │ └── dispatch-discrete-custom-event.ts └── InputSlider │ ├── index.ts │ ├── slots.ts │ ├── components │ ├── index.ts │ ├── Track.tsx │ ├── Range.tsx │ ├── SupremumThumb.tsx │ └── InfimumThumb.tsx │ ├── types.ts │ ├── context.ts │ └── utils.ts ├── tests ├── jest.setup.ts └── utils │ ├── wait.ts │ ├── itShouldMount.tsx │ ├── index.ts │ ├── itSupportsRef.tsx │ ├── itSupportsClassName.tsx │ ├── itSupportsFocusEvents.tsx │ ├── itSupportsStyle.tsx │ ├── itSupportsDataSetProps.tsx │ └── itIsPolymorphic.tsx ├── .eslintignore ├── .prettierignore ├── tsconfig.lint.json ├── .npmrc ├── pages ├── index.tsx ├── _app.tsx └── _document.tsx ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── next.config.js ├── next-env.d.ts ├── .dev └── styles.css ├── tsconfig.build.json ├── .prettierrc ├── jest.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── development.yml │ └── npm-publish.yml ├── tsconfig.json ├── LICENSE ├── scripts ├── minify-package.ts └── ci │ └── publish-package.ts ├── README.md ├── readme-dark-icon.svg ├── readme-light-icon.svg ├── .gitignore ├── .eslintrc └── package.json /lib/Button/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Button:Root"; 2 | -------------------------------------------------------------------------------- /lib/Meter/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Meter:Root"; 2 | -------------------------------------------------------------------------------- /lib/Popper/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Popper:Root"; 2 | -------------------------------------------------------------------------------- /lib/Radio/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Radio:Root"; 2 | -------------------------------------------------------------------------------- /lib/Switch/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Switch:Root"; 2 | -------------------------------------------------------------------------------- /lib/Toggle/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Toggle:Root"; 2 | -------------------------------------------------------------------------------- /lib/Checkbox/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Checkbox:Root"; 2 | -------------------------------------------------------------------------------- /tests/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /lib/CheckGroup/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "CheckGroup:Root"; 2 | -------------------------------------------------------------------------------- /lib/ProgressBar/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "ProgressBar:Root"; 2 | -------------------------------------------------------------------------------- /lib/RadioGroup/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "RadioGroup:Root"; 2 | -------------------------------------------------------------------------------- /lib/ToggleGroup/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "ToggleGroup:Root"; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .swc 4 | .github 5 | .next 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .swc 4 | .github 5 | .next 6 | -------------------------------------------------------------------------------- /lib/As/index.ts: -------------------------------------------------------------------------------- 1 | export { default, type Props as AsProps } from "./As"; 2 | -------------------------------------------------------------------------------- /lib/Portal/index.ts: -------------------------------------------------------------------------------- 1 | export { default, type Props as PortalProps } from "./Portal"; 2 | -------------------------------------------------------------------------------- /lib/ToggleGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { default, type Props as ToggleGroupProps } from "./ToggleGroup"; 2 | -------------------------------------------------------------------------------- /lib/internals/FocusTrap/index.ts: -------------------------------------------------------------------------------- 1 | export { default, type Props as FocusTrapProps } from "./FocusTrap"; 2 | -------------------------------------------------------------------------------- /lib/Menu/components/RadioGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { default, type Props as RadioGroupProps } from "./RadioGroup"; 2 | -------------------------------------------------------------------------------- /lib/internals/FocusRedirect/index.ts: -------------------------------------------------------------------------------- 1 | export { default, type Props as FocusRedirectProps } from "./FocusRedirect"; 2 | -------------------------------------------------------------------------------- /lib/TabGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Root, type Props as RootProps } from "./TabGroup"; 2 | export * from "./components"; 3 | -------------------------------------------------------------------------------- /tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { "noEmit": true, "jsx": "react-jsx" } 4 | } 5 | -------------------------------------------------------------------------------- /lib/Breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Root, type Props as RootProps } from "./Breadcrumb"; 2 | export * from "./components"; 3 | -------------------------------------------------------------------------------- /lib/PreserveAspectRatio/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type Props as PreserveAspectRatioProps, 4 | } from "./PreserveAspectRatio"; 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*eslint* 2 | public-hoist-pattern[]=*prettier* 3 | public-hoist-pattern[]=@types* 4 | enable-pre-post-scripts=true 5 | -------------------------------------------------------------------------------- /lib/PreserveAspectRatio/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "PreserveAspectRatio:Root"; 2 | export const Container = "PreserveAspectRatio:Container"; 3 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | const Page = () => { 2 | return
; 3 | }; 4 | 5 | export default Page; 6 | -------------------------------------------------------------------------------- /lib/TreeView/components/Item/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps, 4 | type Props, 5 | type RenderProps, 6 | } from "./Item"; 7 | -------------------------------------------------------------------------------- /lib/Toast/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Toast:Root"; 2 | export const ContentRoot = "Toast:Content:Root"; 3 | export const ActionRoot = "Toast:Action:Root"; 4 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "outDir": "./dist/esm" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/Expandable/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Expandable:Root"; 2 | export const TriggerRoot = "Expandable:Trigger:Root"; 3 | export const ContentRoot = "Expandable:Content:Root"; 4 | -------------------------------------------------------------------------------- /lib/Portal/utils.ts: -------------------------------------------------------------------------------- 1 | export const getContainer = (querySelector?: string) => 2 | querySelector 3 | ? document.querySelector(querySelector) 4 | : document.body; 5 | -------------------------------------------------------------------------------- /lib/Select/components/Controller/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as ControllerClassNameProps, 4 | type Props as ControllerProps, 5 | } from "./Controller"; 6 | -------------------------------------------------------------------------------- /lib/Toast/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Action, type Props as ActionProps } from "./Action"; 2 | export { default as Content, type Props as ContentProps } from "./Content"; 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("next/dist/server/config").NextConfig} */ 2 | const nextConfig = { reactStrictMode: true, trailingSlash: false }; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /lib/Toast/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | } from "./Toast"; 6 | export * from "./components"; 7 | -------------------------------------------------------------------------------- /tests/utils/wait.ts: -------------------------------------------------------------------------------- 1 | const wait = (durationInMillis: number) => 2 | new Promise(resolve => { 3 | setTimeout(resolve, durationInMillis); 4 | }); 5 | 6 | export default wait; 7 | -------------------------------------------------------------------------------- /lib/Expandable/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Content, type Props as ContentProps } from "./Content"; 2 | export { default as Trigger, type Props as TriggerProps } from "./Trigger"; 3 | -------------------------------------------------------------------------------- /lib/Meter/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as MeterClassNameProps, 4 | type Props as MeterProps, 5 | type RenderProps as MeterRenderProps, 6 | } from "./Meter"; 7 | -------------------------------------------------------------------------------- /lib/Radio/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as RadioClassNameProps, 4 | type Props as RadioProps, 5 | type RenderProps as RadioRenderProps, 6 | } from "./Radio"; 7 | -------------------------------------------------------------------------------- /lib/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as ButtonClassNameProps, 4 | type Props as ButtonProps, 5 | type RenderProps as ButtonRenderProps, 6 | } from "./Button"; 7 | -------------------------------------------------------------------------------- /lib/Popper/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as PopperClassNameProps, 4 | type Props as PopperProps, 5 | type RenderProps as PopperRenderProps, 6 | } from "./Popper"; 7 | -------------------------------------------------------------------------------- /lib/Switch/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as SwitchClassNameProps, 4 | type Props as SwitchProps, 5 | type RenderProps as SwitchRenderProps, 6 | } from "./Switch"; 7 | -------------------------------------------------------------------------------- /lib/Toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as ToggleClassNameProps, 4 | type Props as ToggleProps, 5 | type RenderProps as ToggleRenderProps, 6 | } from "./Toggle"; 7 | -------------------------------------------------------------------------------- /lib/Menu/components/Item/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as ItemClassNameProps, 4 | type Props as ItemProps, 5 | type RenderProps as ItemRenderProps, 6 | } from "./Item"; 7 | -------------------------------------------------------------------------------- /lib/Select/components/List/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as ListClassNameProps, 4 | type Props as ListProps, 5 | type RenderProps as ListRenderProps, 6 | } from "./List"; 7 | -------------------------------------------------------------------------------- /lib/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as TooltipClassNameProps, 4 | type Props as TooltipProps, 5 | type RenderProps as TooltipRenderProps, 6 | } from "./Tooltip"; 7 | -------------------------------------------------------------------------------- /lib/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as CheckboxClassNameProps, 4 | type Props as CheckboxProps, 5 | type RenderProps as CheckboxRenderProps, 6 | } from "./Checkbox"; 7 | -------------------------------------------------------------------------------- /lib/PortalConfigProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type Props as PortalConfigProviderProps, 4 | } from "./PortalConfigProvider"; 5 | export { default as usePortalConfig } from "./usePortalConfig"; 6 | -------------------------------------------------------------------------------- /lib/SpinButton/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "SpinButton:Root"; 2 | export const IncrementButtonRoot = "SpinButton:IncrementButton:Root"; 3 | export const DecrementButtonRoot = "SpinButton:DecrementButton:Root"; 4 | -------------------------------------------------------------------------------- /lib/CheckGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as CheckGroupClassNameProps, 4 | type Props as CheckGroupProps, 5 | type RenderProps as CheckGroupRenderProps, 6 | } from "./CheckGroup"; 7 | -------------------------------------------------------------------------------- /lib/RadioGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as RadioGroupClassNameProps, 4 | type Props as RadioGroupProps, 5 | type RenderProps as RadioGroupRenderProps, 6 | } from "./RadioGroup"; 7 | -------------------------------------------------------------------------------- /lib/ProgressBar/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default, 3 | type ClassNameProps as ProgressBarClassNameProps, 4 | type Props as ProgressBarProps, 5 | type RenderProps as ProgressBarRenderProps, 6 | } from "./ProgressBar"; 7 | -------------------------------------------------------------------------------- /lib/Menu/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./Menu"; 7 | export * from "./components"; 8 | -------------------------------------------------------------------------------- /lib/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./Dialog"; 7 | export * from "./components"; 8 | -------------------------------------------------------------------------------- /lib/Select/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./Select"; 7 | export * from "./components"; 8 | -------------------------------------------------------------------------------- /lib/utils/fork-refs.ts: -------------------------------------------------------------------------------- 1 | import setRef from "./set-ref"; 2 | 3 | const forkRefs = 4 | (...refs: React.Ref[]) => 5 | (instance: T) => 6 | refs.forEach(ref => void setRef(ref, instance)); 7 | 8 | export default forkRefs; 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /lib/Breadcrumb/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Breadcrumb:Root"; 2 | export const ListRoot = "Breadcrumb:List:Root"; 3 | export const ItemRoot = "Breadcrumb:Item:Root"; 4 | export const SeparatorItemRoot = "Breadcrumb:SeparatorItem:Root"; 5 | -------------------------------------------------------------------------------- /lib/Expandable/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./Expandable"; 7 | export * from "./components"; 8 | -------------------------------------------------------------------------------- /lib/SpinButton/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./SpinButton"; 7 | export * from "./components"; 8 | -------------------------------------------------------------------------------- /lib/TreeView/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./TreeView"; 7 | export * from "./components"; 8 | -------------------------------------------------------------------------------- /lib/PortalConfigProvider/usePortalConfig.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PortalConfigContext } from "./context"; 3 | 4 | const usePortalConfig = () => React.useContext(PortalConfigContext); 5 | 6 | export default usePortalConfig; 7 | -------------------------------------------------------------------------------- /lib/utils/use-isomorphic-layout-effect.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useIsomorphicLayoutEffect = 4 | typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; 5 | 6 | export default useIsomorphicLayoutEffect; 7 | -------------------------------------------------------------------------------- /lib/TabGroup/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "TabGroup:Root"; 2 | export const ListRoot = "TabGroup:List:Root"; 3 | export const ListLabel = "TabGroup:List:Label"; 4 | export const PanelRoot = "TabGroup:Panel:Root"; 5 | export const TabRoot = "TabGroup:Tab:Root"; 6 | -------------------------------------------------------------------------------- /lib/Dialog/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Dialog:Root"; 2 | export const BackdropRoot = "Dialog:Backdrop:Root"; 3 | export const TitleRoot = "Dialog:Title:Root"; 4 | export const DescriptionRoot = "Dialog:Description:Root"; 5 | export const ContentRoot = "Dialog:Content:Root"; 6 | -------------------------------------------------------------------------------- /lib/TreeView/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "TreeView:Root"; 2 | export const ItemRoot = "TreeView:Item:Root"; 3 | export const ItemTrigger = "TreeView:Item:Trigger"; 4 | export const SubTreeRoot = "TreeView:SubTree:Root"; 5 | export const SubTreeLabel = "TreeView:SubTree:Label"; 6 | -------------------------------------------------------------------------------- /.dev/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 3 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 4 | } 5 | 6 | *, 7 | *::after, 8 | *::before { 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /lib/Breadcrumb/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Item, type Props as ItemProps } from "./Item"; 2 | export { default as List, type Props as ListProps } from "./List"; 3 | export { 4 | default as SeparatorItem, 5 | type Props as SeparatorItemProps, 6 | } from "./SeparatorItem"; 7 | -------------------------------------------------------------------------------- /lib/InputSlider/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Root, 3 | type ClassNameProps as RootClassNameProps, 4 | type Props as RootProps, 5 | type RenderProps as RootRenderProps, 6 | } from "./InputSlider"; 7 | export * from "./components"; 8 | export type { StopSegment } from "./types"; 9 | -------------------------------------------------------------------------------- /lib/TreeView/components/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Item, 3 | type ClassNameProps as ItemClassNameProps, 4 | type Props as ItemProps, 5 | type RenderProps as ItemRenderProps, 6 | } from "./Item"; 7 | export { default as SubTree, type Props as SubTreeProps } from "./SubTree"; 8 | -------------------------------------------------------------------------------- /lib/internals/prefix-message.ts: -------------------------------------------------------------------------------- 1 | const prefixMessage = (message: string, scope?: string) => { 2 | let prefix = "[StylelessUI]"; 3 | 4 | if (scope) prefix = prefix.concat(`[${scope}]`); 5 | 6 | return `${prefix}: ${message}`; 7 | }; 8 | 9 | export default prefixMessage; 10 | -------------------------------------------------------------------------------- /lib/utils/set-ref.ts: -------------------------------------------------------------------------------- 1 | const setRef = (ref: React.Ref, value: T) => { 2 | if (typeof ref === "function") ref(value); 3 | else if (ref && typeof ref === "object" && "current" in ref) 4 | (ref as React.MutableRefObject).current = value; 5 | }; 6 | 7 | export default setRef; 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "jsx": "react-jsx", 6 | "declaration": true 7 | }, 8 | "include": ["lib/**/*.ts", "lib/**/*.tsx"], 9 | "exclude": ["**/*.test.ts", "**/*.test.tsx"] 10 | } 11 | -------------------------------------------------------------------------------- /lib/InputSlider/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "InputSlider:Root"; 2 | export const TrackRoot = "InputSlider:Track:Root"; 3 | export const RangeRoot = "InputSlider:Range:Root"; 4 | export const InfimumThumbRoot = "InputSlider:Thumb:Infimum:Root"; 5 | export const SupremumThumbRoot = "InputSlider:Thumb:Supremum:Root"; 6 | -------------------------------------------------------------------------------- /lib/utils/get-direction.ts: -------------------------------------------------------------------------------- 1 | import { getWindow } from "./dom"; 2 | 3 | type Direction = "rtl" | "ltr"; 4 | 5 | const getDirection = (element: HTMLElement) => { 6 | const context = getWindow(element); 7 | 8 | return context.getComputedStyle(element).direction as Direction; 9 | }; 10 | 11 | export default getDirection; 12 | -------------------------------------------------------------------------------- /lib/TabGroup/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as List, type Props as ListProps } from "./List"; 2 | export { default as Panel, type Props as PanelProps } from "./Panel"; 3 | export { 4 | default as Tab, 5 | type ClassNameProps as TabClassNameProps, 6 | type Props as TabProps, 7 | type RenderProps as TabRenderProps, 8 | } from "./Tab"; 9 | -------------------------------------------------------------------------------- /lib/utils/create-custom-event.ts: -------------------------------------------------------------------------------- 1 | const createCustomEvent = ( 2 | scope: string, 3 | eventName: string, 4 | eventInit: EventInit, 5 | ) => { 6 | const type = `custom.${scope.toLowerCase()}.${eventName.toLowerCase()}`; 7 | const event = new CustomEvent(type, eventInit); 8 | 9 | return event; 10 | }; 11 | 12 | export default createCustomEvent; 13 | -------------------------------------------------------------------------------- /lib/Dialog/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Backdrop, type Props as BackdropProps } from "./Backdrop"; 2 | export { default as Content, type Props as ContentProps } from "./Content"; 3 | export { 4 | default as Description, 5 | type Props as DescriptionProps, 6 | } from "./Description"; 7 | export { default as Title, type Props as TitleProps } from "./Title"; 8 | -------------------------------------------------------------------------------- /lib/Select/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Select:Root"; 2 | export const ControllerRoot = "Select:Controller:Root"; 3 | export const TriggerRoot = "Select:Trigger:Root"; 4 | export const GroupRoot = "Select:Group:Root"; 5 | export const ListRoot = "Select:List:Root"; 6 | export const OptionRoot = "Select:Option:Root"; 7 | export const EmptyStatementRoot = "Select:EmptyStatement:Root"; 8 | -------------------------------------------------------------------------------- /lib/Menu/constants.ts: -------------------------------------------------------------------------------- 1 | import { createCustomEvent } from "../utils"; 2 | 3 | export const ExpandSubMenuEvent = createCustomEvent("Menu", "expandSubMenu", { 4 | bubbles: true, 5 | cancelable: true, 6 | }); 7 | 8 | export const CollapseSubMenuEvent = createCustomEvent( 9 | "Menu", 10 | "collapseSubMenu", 11 | { 12 | bubbles: true, 13 | cancelable: true, 14 | }, 15 | ); 16 | -------------------------------------------------------------------------------- /lib/Toast/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | role: "alert" | "status"; 5 | open: boolean; 6 | }; 7 | 8 | const Context = React.createContext(null); 9 | 10 | if (process.env.NODE_ENV !== "production") { 11 | Context.displayName = "ToastContext"; 12 | } 13 | 14 | export { Context as ToastContext, type ContextValue as ToastContextValue }; 15 | -------------------------------------------------------------------------------- /lib/Menu/slots.ts: -------------------------------------------------------------------------------- 1 | export const Root = "Menu:Root"; 2 | export const SubMenuRoot = "Menu:SubMenu:Root"; 3 | export const SeparatorItemRoot = "Menu:SeparatorItem:Root"; 4 | export const RadioItemRoot = "Menu:RadioItem:Root"; 5 | export const RadioGroupRoot = "Menu:RadioGroup:Root"; 6 | export const ItemRoot = "Menu:Item:Root"; 7 | export const GroupRoot = "Menu:Group:Root"; 8 | export const CheckItemRoot = "Menu:CheckItem:Root"; 9 | -------------------------------------------------------------------------------- /lib/utils/use-is-initial-render-complete.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const useIsInitialRenderComplete = () => { 4 | const [isInitialRenderComplete, setIsInitialRenderComplete] = 5 | React.useState(false); 6 | 7 | React.useEffect(() => { 8 | setIsInitialRenderComplete(true); 9 | }, []); 10 | 11 | return isInitialRenderComplete; 12 | }; 13 | 14 | export default useIsInitialRenderComplete; 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "semi": true, 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid", 9 | "bracketSameLine": false, 10 | "endOfLine": "lf", 11 | "htmlWhitespaceSensitivity": "css", 12 | "jsxSingleQuote": false, 13 | "singleAttributePerLine": true, 14 | "plugins": ["prettier-plugin-organize-imports"] 15 | } 16 | -------------------------------------------------------------------------------- /lib/internals/SystemError.ts: -------------------------------------------------------------------------------- 1 | import prefixMessage from "./prefix-message"; 2 | 3 | class SystemError extends Error { 4 | constructor(err: Error | string, scope?: string) { 5 | const message = typeof err === "string" ? err : err.message; 6 | const prefixedMessage = prefixMessage(message, scope); 7 | 8 | super(prefixedMessage); 9 | 10 | this.name = "SystemError"; 11 | } 12 | } 13 | 14 | export default SystemError; 15 | -------------------------------------------------------------------------------- /lib/internals/resolve-prop-with-render-context.ts: -------------------------------------------------------------------------------- 1 | const resolvePropWithRenderContext = ( 2 | prop: TProp | ((renderContext: TRenderContext) => TProp), 3 | renderContext: TRenderContext, 4 | ) => { 5 | if (typeof prop === "function") { 6 | return (prop as (renderContext: TRenderContext) => TProp)(renderContext); 7 | } 8 | 9 | return prop; 10 | }; 11 | 12 | export default resolvePropWithRenderContext; 13 | -------------------------------------------------------------------------------- /lib/Menu/components/Item/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | id: string; 5 | isExpanded: boolean; 6 | }; 7 | 8 | const Context = React.createContext(null); 9 | 10 | if (process.env.NODE_ENV !== "production") { 11 | Context.displayName = "Menu.Item.Context"; 12 | } 13 | 14 | export { 15 | Context as MenuItemContext, 16 | type ContextValue as MenuItemContextValue, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/Dialog/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | role: "dialog" | "alertdialog"; 5 | open: boolean; 6 | emitClose: () => void; 7 | }; 8 | 9 | const Context = React.createContext(null); 10 | 11 | if (process.env.NODE_ENV !== "production") { 12 | Context.displayName = "DialogContext"; 13 | } 14 | 15 | export { Context as DialogContext, type ContextValue as DialogContextValue }; 16 | -------------------------------------------------------------------------------- /lib/internals/keys.ts: -------------------------------------------------------------------------------- 1 | export const SystemKeys = { 2 | BACKSPACE: "Backspace", 3 | TAB: "Tab", 4 | ENTER: "Enter", 5 | SHIFT: "Shift", 6 | CONTROL: "Control", 7 | ALT: "Alt", 8 | META: "Meta", 9 | ESCAPE: "Escape", 10 | SPACE: " ", 11 | LEFT: "ArrowLeft", 12 | RIGHT: "ArrowRight", 13 | UP: "ArrowUp", 14 | DOWN: "ArrowDown", 15 | DELETE: "Delete", 16 | HOME: "Home", 17 | END: "End", 18 | PAGE_UP: "PageUp", 19 | PAGE_DOWN: "PageDown", 20 | }; 21 | -------------------------------------------------------------------------------- /lib/Expandable/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | isExpanded: boolean; 5 | emitExpandChange: (expandState: boolean) => void; 6 | }; 7 | 8 | const Context = React.createContext(null); 9 | 10 | if (process.env.NODE_ENV !== "production") { 11 | Context.displayName = "ExpandableContext"; 12 | } 13 | 14 | export { 15 | Context as ExpandableContext, 16 | type ContextValue as ExpandableContextValue, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/utils/create-virtual-element.ts: -------------------------------------------------------------------------------- 1 | import type { VirtualElement } from "../types"; 2 | 3 | const createVirtualElement = ( 4 | width: number, 5 | height: number, 6 | x: number, 7 | y: number, 8 | ): VirtualElement => ({ 9 | getBoundingClientRect: () => ({ 10 | width, 11 | height, 12 | x, 13 | y, 14 | left: x, 15 | top: y, 16 | right: width + x, 17 | bottom: height + y, 18 | }), 19 | }); 20 | 21 | export default createVirtualElement; 22 | -------------------------------------------------------------------------------- /lib/Menu/components/RadioGroup/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | value: string; 5 | onValueChange: (value: string) => void; 6 | }; 7 | 8 | const Context = React.createContext(null); 9 | 10 | if (process.env.NODE_ENV !== "production") { 11 | Context.displayName = "Menu.RadioGroup.Context"; 12 | } 13 | 14 | export { 15 | Context as RadioGroupContext, 16 | type ContextValue as RadioGroupContextValue, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/TreeView/components/Item/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | id: string; 5 | isExpanded: boolean; 6 | value: string; 7 | }; 8 | 9 | const Context = React.createContext(null); 10 | 11 | if (process.env.NODE_ENV !== "production") { 12 | Context.displayName = "TreeView.Item.Context"; 13 | } 14 | 15 | export { 16 | Context as TreeViewItemContext, 17 | type ContextValue as TreeViewItemContextValue, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/SpinButton/components/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as DecrementButton, 3 | type ClassNameProps as DecrementButtonClassNameProps, 4 | type Props as DecrementButtonProps, 5 | type RenderProps as DecrementButtonRenderProps, 6 | } from "./DecrementButton"; 7 | export { 8 | default as IncrementButton, 9 | type ClassNameProps as IncrementButtonClassNameProps, 10 | type Props as IncrementButtonProps, 11 | type RenderProps as IncrementButtonRenderProps, 12 | } from "./IncrementButton"; 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | import nextJest from "next/jest.js"; 2 | 3 | const createJestConfig = nextJest({ dir: "./" }); 4 | 5 | /** 6 | * @type {import("jest").Config} 7 | */ 8 | const jestConfig = { 9 | verbose: true, 10 | setupFilesAfterEnv: ["/tests/jest.setup.ts"], 11 | testPathIgnorePatterns: ["/.next/", "/node_modules/"], 12 | testRegex: ".*\\.test\\.tsx?$", 13 | testEnvironment: "jest-environment-jsdom", 14 | preset: "ts-jest", 15 | }; 16 | 17 | export default createJestConfig(jestConfig); 18 | -------------------------------------------------------------------------------- /lib/PortalConfigProvider/PortalConfigProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PortalConfigContext, type PortalConfigContextValue } from "./context"; 3 | 4 | export type Props = { 5 | children: React.ReactNode; 6 | config: PortalConfigContextValue; 7 | }; 8 | 9 | const PortalConfigProvider = (props: Props) => { 10 | const { config, children } = props; 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default PortalConfigProvider; 20 | -------------------------------------------------------------------------------- /lib/utils/component-with-forwarded-ref.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as React from "react"; 3 | 4 | const componentWithForwardedRef = < 5 | TComponent extends React.ForwardRefRenderFunction, 6 | >( 7 | component: TComponent, 8 | name: string, 9 | ): React.FC[0]> => { 10 | const forwarded: React.FC[0]> = 11 | React.forwardRef(component); 12 | 13 | forwarded.displayName = name; 14 | 15 | return forwarded; 16 | }; 17 | 18 | export default componentWithForwardedRef; 19 | -------------------------------------------------------------------------------- /tests/utils/itShouldMount.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "."; 3 | 4 | const itShouldMount =

( 5 | Component: React.ComponentType

, 6 | requiredProps: P, 7 | ): void => { 8 | it(`component could be mounted and unmounted without errors`, () => { 9 | const elem = () as React.ReactElement; 10 | 11 | const result = render(elem); 12 | 13 | expect(() => { 14 | result.rerender(elem); 15 | result.unmount(); 16 | }).not.toThrow(); 17 | }); 18 | }; 19 | 20 | export default itShouldMount; 21 | -------------------------------------------------------------------------------- /lib/InputSlider/components/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as InfimumThumb, 3 | type ClassNameProps as InfimumThumbClassNameProps, 4 | type Props as InfimumThumbProps, 5 | type RenderProps as InfimumThumbRenderProps, 6 | } from "./InfimumThumb"; 7 | export { default as Range, type Props as RangeProps } from "./Range"; 8 | export { 9 | default as SupremumThumb, 10 | type ClassNameProps as SupremumThumbClassNameProps, 11 | type Props as SupremumThumbProps, 12 | type RenderProps as SupremumThumbRenderProps, 13 | } from "./SupremumThumb"; 14 | export { default as Track, type Props as TrackProps } from "./Track"; 15 | -------------------------------------------------------------------------------- /lib/PortalConfigProvider/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | /** 5 | * A function that will resolve the container element for the portals. 6 | * 7 | * Please note that this function is only called on the client-side. 8 | */ 9 | resolveContainer: () => HTMLElement | null; 10 | }; 11 | 12 | const Context = React.createContext(null); 13 | 14 | if (process.env.NODE_ENV !== "production") 15 | Context.displayName = "PortalConfigContext"; 16 | 17 | export { 18 | Context as PortalConfigContext, 19 | type ContextValue as PortalConfigContextValue, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/use-isomorphic-value.ts: -------------------------------------------------------------------------------- 1 | import useIsServerHandoffComplete from "@utilityjs/use-is-server-handoff-complete"; 2 | 3 | const getValue = (valueOrFn: T | (() => T)) => { 4 | if (typeof valueOrFn === "function") return (valueOrFn as () => T)(); 5 | 6 | return valueOrFn; 7 | }; 8 | 9 | const useIsomorphicValue = ( 10 | clientValue: T | (() => T), 11 | serverValue: T | (() => T), 12 | ) => { 13 | const isServerHandoffComplete = useIsServerHandoffComplete(); 14 | 15 | if (isServerHandoffComplete) return getValue(clientValue); 16 | 17 | return getValue(serverValue); 18 | }; 19 | 20 | export default useIsomorphicValue; 21 | -------------------------------------------------------------------------------- /lib/CheckGroup/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { PickAsMandatory } from "../types"; 3 | import { type Props } from "./CheckGroup"; 4 | 5 | type ContextValue = PickAsMandatory & 6 | Pick & { 7 | onChange: (newCheckedState: boolean, inputValue: string) => void; 8 | }; 9 | 10 | const Context = React.createContext(null); 11 | 12 | if (process.env.NODE_ENV !== "production") { 13 | Context.displayName = "CheckGroupContext"; 14 | } 15 | 16 | export { 17 | Context as CheckGroupContext, 18 | type ContextValue as CheckGroupContextValue, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/Menu/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { PopperProps } from "../Popper"; 3 | 4 | type ContextValue = { 5 | id: string; 6 | activeElement: HTMLElement | null; 7 | keepMounted: boolean; 8 | emitClose: () => void; 9 | emitActiveElementChange: (newActiveElement: HTMLElement | null) => void; 10 | computationMiddleware: NonNullable; 11 | }; 12 | 13 | const Context = React.createContext(null); 14 | 15 | if (process.env.NODE_ENV !== "production") { 16 | Context.displayName = "Menu.Context"; 17 | } 18 | 19 | export { Context as MenuContext, type ContextValue as MenuContextValue }; 20 | -------------------------------------------------------------------------------- /tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { act, render, screen } from "@testing-library/react"; 2 | export { default as userEvent } from "@testing-library/user-event"; 3 | export { default as itIsPolymorphic } from "./itIsPolymorphic"; 4 | export { default as itShouldMount } from "./itShouldMount"; 5 | export { default as itSupportsClassName } from "./itSupportsClassName"; 6 | export { default as itSupportsDataSetProps } from "./itSupportsDataSetProps"; 7 | export { default as itSupportsFocusEvents } from "./itSupportsFocusEvents"; 8 | export { default as itSupportsRef } from "./itSupportsRef"; 9 | export { default as itSupportsStyle } from "./itSupportsStyle"; 10 | export { default as wait } from "./wait"; 11 | -------------------------------------------------------------------------------- /lib/RadioGroup/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { PickAsMandatory } from "../types"; 3 | import { type Props } from "./RadioGroup"; 4 | 5 | type ContextValue = PickAsMandatory & 6 | Pick & { 7 | forcedTabability: string | null; 8 | onChange: (newCheckedState: boolean, inputValue: string) => void; 9 | }; 10 | 11 | const Context = React.createContext(null); 12 | 13 | if (process.env.NODE_ENV !== "production") { 14 | Context.displayName = "RadioGroupContext"; 15 | } 16 | 17 | export { 18 | Context as RadioGroupContext, 19 | type ContextValue as RadioGroupContextValue, 20 | }; 21 | -------------------------------------------------------------------------------- /tests/utils/itSupportsRef.tsx: -------------------------------------------------------------------------------- 1 | // Cherry picked from https://github.com/mantinedev/mantine/blob/master/src/mantine-tests/src/it-supports-ref.tsx 2 | 3 | import * as React from "react"; 4 | import { render } from "."; 5 | 6 | const itSupportsRef = ( 7 | Component: React.ComponentType, 8 | requiredProps: T, 9 | refType: unknown, 10 | ): void => { 11 | it(`supports forwarding ref`, () => { 12 | const ref = React.createRef(); 13 | 14 | render( 15 | , 19 | ); 20 | expect(ref.current).toBeInstanceOf(refType); 21 | }); 22 | }; 23 | 24 | export default itSupportsRef; 25 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { type AppProps } from "next/app"; 2 | import Head from "next/head"; 3 | import "../.dev/styles.css"; 4 | 5 | const _App = (props: AppProps) => { 6 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 7 | const { Component: Page, pageProps } = props; 8 | 9 | return ( 10 | <> 11 | 12 | 17 | 18 |

19 | 20 |
21 | 22 | ); 23 | }; 24 | 25 | export default _App; 26 | -------------------------------------------------------------------------------- /lib/SpinButton/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { PickAsMandatory } from "../types"; 3 | import type { Props } from "./SpinButton"; 4 | 5 | type ContextValue = PickAsMandatory & { 6 | isUpperBoundDisabled: boolean; 7 | isLowerBoundDisabled: boolean; 8 | handleIncrease: (step: number) => void; 9 | handleDecrease: (step: number) => void; 10 | }; 11 | 12 | const Context = React.createContext(null); 13 | 14 | if (process.env.NODE_ENV !== "production") { 15 | Context.displayName = "SpinButtonContext"; 16 | } 17 | 18 | export { 19 | Context as SpinButtonContext, 20 | type ContextValue as SpinButtonContextValue, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/utils/use-event-callback.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * A community-wide workaround for `useCallback()`. 5 | * Because the `useCallback()` hook invalidates too often in practice. 6 | * 7 | * https://github.com/facebook/react/issues/14099#issuecomment-440013892 8 | */ 9 | const useEventCallback = < 10 | E extends React.BaseSyntheticEvent | Event, 11 | T extends (event: E) => void = (event: E) => void, 12 | >( 13 | fn: T, 14 | ): T => { 15 | const ref = React.useRef(fn); 16 | 17 | React.useEffect(() => void (ref.current = fn)); 18 | 19 | return React.useCallback((event: E) => void ref.current(event), []) as T; 20 | }; 21 | 22 | export default useEventCallback; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /lib/internals/index.ts: -------------------------------------------------------------------------------- 1 | export type * from "./FocusRedirect"; 2 | export { default as FocusRedirect } from "./FocusRedirect"; 3 | export type * from "./FocusTrap"; 4 | export { default as FocusTrap } from "./FocusTrap"; 5 | export { default as SystemError } from "./SystemError"; 6 | export * from "./get-label-info"; 7 | export { default as getLabelInfo } from "./get-label-info"; 8 | export * from "./keys"; 9 | export { default as logger } from "./logger"; 10 | export { default as prefixMessage } from "./prefix-message"; 11 | export { default as resolvePropWithRenderContext } from "./resolve-prop-with-render-context"; 12 | export * from "./styles"; 13 | export { default as useJumpToChar } from "./use-jump-to-char"; 14 | -------------------------------------------------------------------------------- /lib/internals/styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from "react"; 2 | 3 | export const visuallyHiddenCSSProperties = { 4 | position: "absolute", 5 | width: 1, 6 | height: 1, 7 | padding: 0, 8 | margin: -1, 9 | border: 0, 10 | overflow: "hidden", 11 | clip: "rect(0, 0, 0, 0)", 12 | whiteSpace: "nowrap", 13 | } as CSSProperties; 14 | 15 | export const disableUserSelectCSSProperties = { 16 | WebkitUserSelect: "none", 17 | MozUserSelect: "none", 18 | MsUserSelect: "none", 19 | KhtmlUserSelect: "none", 20 | userSelect: "none", 21 | WebkitTouchCallout: "none", 22 | MsTouchAction: "pan-y", 23 | touchAction: "pan-y", 24 | WebkitTapHighlightColor: "transparent", 25 | } as CSSProperties; 26 | -------------------------------------------------------------------------------- /tests/utils/itSupportsClassName.tsx: -------------------------------------------------------------------------------- 1 | // Cherry picked from https://github.com/mantinedev/mantine/blob/master/src/mantine-tests/src/it-supports-classname.tsx 2 | 3 | import * as React from "react"; 4 | import { render } from "."; 5 | 6 | const cls = "styleless-component-test-classname"; 7 | 8 | const itSupportsClassName = ( 9 | Component: React.ComponentType, 10 | requiredProps: T, 11 | ): void => { 12 | it("supports className prop", () => { 13 | const { container } = render( 14 | , 18 | ); 19 | 20 | expect(container.firstChild).toHaveClass(cls); 21 | }); 22 | }; 23 | 24 | export default itSupportsClassName; 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Bug 8 | 9 | - [ ] Related issues linked using `fixes #number` 10 | - [ ] Tests added 11 | 12 | ## Feature 13 | 14 | - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. 15 | - [ ] Related issues linked using `fixes #number` 16 | - [ ] Tests added 17 | - [ ] Documentation added 18 | - [ ] Telemetry added. In case of a feature if it's used or not. 19 | -------------------------------------------------------------------------------- /lib/ToggleGroup/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { type Props } from "./ToggleGroup"; 3 | 4 | type ContextValue = { 5 | multiple: boolean; 6 | forcedTabability: string | null; 7 | keyboardActivationBehavior: Exclude< 8 | Props["keyboardActivationBehavior"], 9 | undefined 10 | >; 11 | value: Exclude; 12 | onChange: (newActiveState: boolean, toggleValue: string) => void; 13 | }; 14 | 15 | const Context = React.createContext(null); 16 | 17 | if (process.env.NODE_ENV !== "production") { 18 | Context.displayName = "ToggleGroupContext"; 19 | } 20 | 21 | export { 22 | Context as ToggleGroupContext, 23 | type ContextValue as ToggleGroupContextValue, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/As/As.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { componentWithForwardedRef } from "../utils"; 3 | import AsClone from "./AsClone"; 4 | 5 | type OwnProps = { 6 | /** 7 | * The content of the component. It should be a single non-fragment React element. 8 | */ 9 | children: React.ReactElement; 10 | }; 11 | 12 | export type Props = React.HTMLAttributes & OwnProps; 13 | 14 | const AsBase = (props: Props, ref: React.Ref) => { 15 | const { children, ...otherProps } = props; 16 | 17 | return ( 18 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | const As = componentWithForwardedRef(AsBase, "As"); 28 | 29 | export default As; 30 | -------------------------------------------------------------------------------- /lib/TabGroup/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type ContextValue = { 4 | activeTab: string; 5 | orientation: "horizontal" | "vertical"; 6 | forcedTabability: string | null; 7 | keyboardActivationBehavior: "manual" | "automatic"; 8 | onChange: (tabValue: string) => void; 9 | }; 10 | 11 | const Context = React.createContext(null); 12 | 13 | const ListContext = React.createContext(false); 14 | 15 | if (process.env.NODE_ENV !== "production") { 16 | Context.displayName = "TabGroup.Context"; 17 | ListContext.displayName = "TabGroup.List.Context"; 18 | } 19 | 20 | export { 21 | Context as TabGroupContext, 22 | ListContext as TabGroupListContext, 23 | type ContextValue as TabGroupContextValue, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/internals/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import type { AnyFunction } from "../types"; 3 | import prefixMessage from "./prefix-message"; 4 | 5 | type Type = "error" | "warn" | "default"; 6 | 7 | type Options = { 8 | scope: string; 9 | type: Type; 10 | }; 11 | 12 | const logger = (message: string, options?: Partial) => { 13 | const { scope, type = "default" } = options ?? {}; 14 | 15 | const prefixedMessage = prefixMessage(message, scope); 16 | 17 | const mapTypeToLoggerFn = { 18 | error: console.error, 19 | warn: console.warn, 20 | default: console.log, 21 | } satisfies Record; 22 | 23 | const loggerFn = mapTypeToLoggerFn[type]; 24 | 25 | loggerFn(prefixedMessage); 26 | }; 27 | 28 | export default logger; 29 | -------------------------------------------------------------------------------- /lib/InputSlider/types.ts: -------------------------------------------------------------------------------- 1 | export type ThumbNames = "infimum" | "supremum"; 2 | export type Entities = ThumbNames | "range"; 3 | 4 | export type ThumbState = { 5 | active: boolean; 6 | zIndex: number; 7 | }; 8 | 9 | export type Positions = Record & { 10 | range: { start: number; end: number }; 11 | }; 12 | 13 | export type ThumbInfo = { 14 | index: 0 | 1; 15 | name: ThumbNames; 16 | value: number; 17 | minValue: number; 18 | maxValue: number; 19 | state: ThumbState; 20 | ref: React.RefObject; 21 | setState: React.Dispatch>; 22 | }; 23 | 24 | export type ThumbsInfo = Record; 25 | 26 | export type Orientation = "horizontal" | "vertical"; 27 | 28 | export type StopSegment = { length: number; index: number }; 29 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - "next" 7 | pull_request: 8 | schedule: 9 | # on sunday of each month at 5:55 10 | - cron: "55 5 * * 0" 11 | workflow_call: 12 | 13 | jobs: 14 | analyze: 15 | name: "Analyze" 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: 21 | - javascript 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: "🎬 Initialize CodeQL" 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: ${{ matrix.language }} 29 | 30 | - name: "🏗️ Autobuild" 31 | uses: github/codeql-action/autobuild@v3 32 | 33 | - name: "🧐 Perform CodeQL Analysis" 34 | uses: github/codeql-action/analyze@v3 35 | -------------------------------------------------------------------------------- /lib/Select/components/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as Controller, 3 | type ControllerClassNameProps, 4 | type ControllerProps, 5 | } from "./Controller"; 6 | export { 7 | default as EmptyStatement, 8 | type Props as EmptyStatementProps, 9 | } from "./EmptyStatement"; 10 | export { 11 | default as Group, 12 | type ClassNameProps as GroupClassNameProps, 13 | type Props as GroupProps, 14 | type RenderProps as GroupRenderProps, 15 | } from "./Group"; 16 | export { 17 | default as List, 18 | type ListClassNameProps, 19 | type ListProps, 20 | type ListRenderProps, 21 | } from "./List"; 22 | export { 23 | default as Option, 24 | type ClassNameProps as OptionClassNameProps, 25 | type Props as OptionProps, 26 | type RenderProps as OptionRenderProps, 27 | } from "./Option"; 28 | export { default as Trigger, type Props as TriggerProps } from "./Trigger"; 29 | -------------------------------------------------------------------------------- /lib/Menu/components/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as CheckItem, 3 | type ClassNameProps as CheckItemClassNameProps, 4 | type Props as CheckItemProps, 5 | type RenderProps as CheckItemRenderProps, 6 | } from "./CheckItem"; 7 | export { default as Group, type Props as GroupProps } from "./Group"; 8 | export { 9 | default as Item, 10 | type ItemClassNameProps, 11 | type ItemProps, 12 | type ItemRenderProps, 13 | } from "./Item"; 14 | export { default as RadioGroup, type RadioGroupProps } from "./RadioGroup"; 15 | export { 16 | default as RadioItem, 17 | type ClassNameProps as RadioItemClassNameProps, 18 | type Props as RadioItemProps, 19 | type RenderProps as RadioItemRenderProps, 20 | } from "./RadioItem"; 21 | export { 22 | default as SeparatorItem, 23 | type Props as SeparatorItemProps, 24 | } from "./SeparatorItem"; 25 | export { default as SubMenu, type Props as SubMenuProps } from "./SubMenu"; 26 | -------------------------------------------------------------------------------- /lib/InputSlider/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { PickAsMandatory } from "../types"; 3 | import { type Props } from "./InputSlider"; 4 | import type { Positions, ThumbsInfo } from "./types"; 5 | 6 | type ContextValue = PickAsMandatory< 7 | Props, 8 | "orientation" | "disabled" | "readOnly" | "setThumbValueText" | "multiThumb" 9 | > & { 10 | getThumbsInfo: () => ThumbsInfo; 11 | getPositions: () => Positions; 12 | handleThumbDragStart: ( 13 | event: React.MouseEvent | React.TouchEvent, 14 | ) => void; 15 | handleThumbKeyDown: React.KeyboardEventHandler; 16 | }; 17 | 18 | const Context = React.createContext(null); 19 | 20 | if (process.env.NODE_ENV !== "production") { 21 | Context.displayName = "InputSliderContext"; 22 | } 23 | 24 | export { 25 | Context as InputSliderContext, 26 | type ContextValue as InputSliderContextValue, 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "pretty": true, 4 | "strictNullChecks": true, 5 | "noFallthroughCasesInSwitch": true, 6 | "noImplicitReturns": true, 7 | "noImplicitOverride": true, 8 | "noUnusedLocals": true, 9 | "noImplicitAny": true, 10 | "noUnusedParameters": true, 11 | "incremental": true, 12 | "target": "ES5", 13 | "useDefineForClassFields": true, 14 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 15 | "allowJs": false, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "module": "ESNext", 22 | "moduleResolution": "Node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "noUncheckedIndexedAccess": true, 26 | "noEmit": true, 27 | "jsx": "preserve" 28 | }, 29 | "include": ["**/*.ts", "**/*.tsx"], 30 | "exclude": ["node_modules", "dist"] 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /lib/utils/request-form-submit.ts: -------------------------------------------------------------------------------- 1 | const isSubmitAction = (element: Element): element is HTMLButtonElement => { 2 | const isHTMLInputElement = (element: Element): element is HTMLInputElement => 3 | element.tagName === "INPUT"; 4 | 5 | const isHTMLButtonElement = ( 6 | element: Element, 7 | ): element is HTMLButtonElement => element.tagName === "BUTTON"; 8 | 9 | return ( 10 | (isHTMLInputElement(element) && element.type === "submit") || 11 | (isHTMLButtonElement(element) && element.type === "submit") || 12 | (isHTMLInputElement(element) && element.type === "image") 13 | ); 14 | }; 15 | 16 | const requestFormSubmit = (element: E) => { 17 | const form = 18 | element instanceof HTMLInputElement 19 | ? element.form 20 | : element.closest("form"); 21 | 22 | if (!form) return; 23 | 24 | Array.from(form.elements).forEach(formElement => { 25 | if (isSubmitAction(formElement)) return formElement.click(); 26 | }); 27 | }; 28 | 29 | export default requestFormSubmit; 30 | -------------------------------------------------------------------------------- /lib/Menu/components/SeparatorItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { MergeElementProps } from "../../types"; 3 | import { componentWithForwardedRef } from "../../utils"; 4 | import { SeparatorItemRoot as SeparatorItemRootSlot } from "../slots"; 5 | 6 | type OwnProps = { 7 | /** 8 | * The className applied to the component. 9 | */ 10 | className?: string; 11 | }; 12 | 13 | export type Props = Omit< 14 | MergeElementProps<"div", OwnProps>, 15 | "defaultValue" | "defaultChecked" | "children" 16 | >; 17 | 18 | const SeparatorItemBase = (props: Props, ref: React.Ref) => { 19 | const { className, ...otherProps } = props; 20 | 21 | return ( 22 |
29 | ); 30 | }; 31 | 32 | const SeparatorItem = componentWithForwardedRef( 33 | SeparatorItemBase, 34 | "Menu.SeparatorItem", 35 | ); 36 | 37 | export default SeparatorItem; 38 | -------------------------------------------------------------------------------- /lib/Breadcrumb/components/Item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { MergeElementProps } from "../../types"; 3 | import { componentWithForwardedRef } from "../../utils"; 4 | import { ItemRoot as ItemRootSlot } from "../slots"; 5 | 6 | type OwnProps = { 7 | /** 8 | * The content of the component. 9 | */ 10 | children?: React.ReactNode; 11 | /** 12 | * The className applied to the component. 13 | */ 14 | className?: string; 15 | }; 16 | 17 | export type Props = Omit< 18 | MergeElementProps<"li", OwnProps>, 19 | "defaultChecked" | "defaultValue" 20 | >; 21 | 22 | const ItemBase = (props: Props, ref: React.Ref) => { 23 | const { className, children, ...otherProps } = props; 24 | 25 | return ( 26 |
  • 32 | {children} 33 |
  • 34 | ); 35 | }; 36 | 37 | const Item = componentWithForwardedRef(ItemBase, "Breadcrumb.Item"); 38 | 39 | export default Item; 40 | -------------------------------------------------------------------------------- /tests/utils/itSupportsFocusEvents.tsx: -------------------------------------------------------------------------------- 1 | // Cherry picked from https://github.com/mantinedev/mantine/blob/master/src/mantine-tests/src/it-supports-focus-events.tsx 2 | 3 | import { fireEvent } from "@testing-library/react"; 4 | import * as React from "react"; 5 | import { render } from "."; 6 | 7 | const itSupportsFocusEvents = ( 8 | Component: React.ComponentType, 9 | requiredProps: T, 10 | selector: string, 11 | ): void => { 12 | it("supports focus events", () => { 13 | const onFocusSpy = jest.fn(); 14 | const onBlurSpy = jest.fn(); 15 | 16 | const { container } = render( 17 | , 22 | ); 23 | 24 | fireEvent.focus(container.querySelector(selector) as Element); 25 | expect(onFocusSpy).toHaveBeenCalled(); 26 | 27 | fireEvent.blur(container.querySelector(selector) as Element); 28 | expect(onBlurSpy).toHaveBeenCalled(); 29 | }); 30 | }; 31 | 32 | export default itSupportsFocusEvents; 33 | -------------------------------------------------------------------------------- /lib/utils/get-scrolling-state.ts: -------------------------------------------------------------------------------- 1 | const getScrollingState = (element: HTMLElement) => { 2 | const isScrollable = (node: HTMLElement) => { 3 | const overflow = getComputedStyle(node).getPropertyValue("overflow"); 4 | 5 | return overflow.includes("auto") || overflow.includes("scroll"); 6 | }; 7 | 8 | const getScrollParent = (element: HTMLElement) => { 9 | let current = element.parentNode as HTMLElement | null; 10 | 11 | while (current) { 12 | if (!(element instanceof HTMLElement)) break; 13 | if (!(element instanceof SVGElement)) break; 14 | 15 | if (isScrollable(current)) return current; 16 | 17 | current = current.parentNode as HTMLElement | null; 18 | } 19 | 20 | return document.scrollingElement || document.documentElement; 21 | }; 22 | 23 | const scrollParent = getScrollParent(element); 24 | 25 | return { 26 | vertical: scrollParent.scrollHeight > scrollParent.clientHeight, 27 | horizontal: scrollParent.scrollWidth > scrollParent.clientWidth, 28 | }; 29 | }; 30 | 31 | export default getScrollingState; 32 | -------------------------------------------------------------------------------- /lib/PreserveAspectRatio/PreserveAspectRatio.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as Slots from "./slots"; 3 | 4 | export type Props = { 5 | /** 6 | * The content of the component. 7 | */ 8 | children?: React.ReactNode; 9 | /** 10 | * The ratio which needs to be preserved. 11 | */ 12 | ratio: number; 13 | }; 14 | 15 | const PreserveAspectRatio = (props: Props) => { 16 | const { children, ratio } = props; 17 | 18 | const rootStyles: React.CSSProperties = { 19 | position: "relative", 20 | paddingTop: `${100 / ratio}%`, 21 | width: "100%", 22 | }; 23 | 24 | const containerStyles: React.CSSProperties = { 25 | position: "absolute", 26 | top: 0, 27 | left: 0, 28 | right: 0, 29 | bottom: 0, 30 | }; 31 | 32 | return ( 33 |
    37 |
    41 | {children} 42 |
    43 |
    44 | ); 45 | }; 46 | 47 | export default PreserveAspectRatio; 48 | -------------------------------------------------------------------------------- /lib/Toast/components/Action.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Button from "../../Button"; 3 | import type { PolymorphicComponent, PolymorphicProps } from "../../types"; 4 | import { componentWithForwardedRef } from "../../utils"; 5 | import { ActionRoot as ActionRootSlot } from "../slots"; 6 | 7 | export type Props> = 8 | PolymorphicProps; 9 | 10 | const ActionBase = < 11 | E extends React.ElementType = typeof Button<"button">, 12 | R extends HTMLElement = HTMLButtonElement, 13 | >( 14 | props: Props, 15 | ref: React.Ref, 16 | ) => { 17 | type TProps = Props>; 18 | 19 | const { as: RootNode = Button<"button">, ...otherProps } = props as TProps; 20 | 21 | return ( 22 | 28 | ); 29 | }; 30 | 31 | const Action: PolymorphicComponent<"button"> = componentWithForwardedRef( 32 | ActionBase, 33 | "Toast.Action", 34 | ); 35 | 36 | export default Action; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 styleless-ui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/minify-package.ts: -------------------------------------------------------------------------------- 1 | import glob from "fast-glob"; 2 | import * as fs from "node:fs/promises"; 3 | import * as path from "node:path"; 4 | import { minify } from "terser"; 5 | 6 | const packagePath = process.cwd(); 7 | 8 | const distPath = path.join(packagePath, "./dist"); 9 | 10 | const minifyPackageFiles = async (files: string[]) => { 11 | for (const file of files) { 12 | const isESModule = path.relative(distPath, file).split("/")[0] === "esm"; 13 | const isIndex = path.basename(file) === "index.js"; 14 | 15 | if (isIndex) continue; 16 | 17 | const source = await fs.readFile(file, { encoding: "utf8" }); 18 | 19 | const result = await minify( 20 | source, 21 | isESModule 22 | ? { 23 | module: isESModule, 24 | compress: { module: isESModule }, 25 | mangle: { module: isESModule }, 26 | } 27 | : undefined, 28 | ); 29 | 30 | if (result.code) await fs.writeFile(file, result.code); 31 | } 32 | }; 33 | 34 | void (async () => { 35 | const files = await glob(path.join(distPath, "**/*.js")); 36 | 37 | await minifyPackageFiles(files); 38 | })(); 39 | -------------------------------------------------------------------------------- /lib/Breadcrumb/components/SeparatorItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { MergeElementProps } from "../../types"; 3 | import { componentWithForwardedRef } from "../../utils"; 4 | import { SeparatorItemRoot as SeparatorItemRootSlot } from "../slots"; 5 | 6 | type OwnProps = { 7 | /** 8 | * The symbol which is used as separator. 9 | */ 10 | separatorSymbol: JSX.Element | string; 11 | /** 12 | * The className applied to the component. 13 | */ 14 | className?: string; 15 | }; 16 | 17 | export type Props = Omit< 18 | MergeElementProps<"li", OwnProps>, 19 | "defaultChecked" | "defaultValue" | "children" 20 | >; 21 | 22 | const SeparatorItemBase = (props: Props, ref: React.Ref) => { 23 | const { className, separatorSymbol, ...otherProps } = props; 24 | 25 | return ( 26 |
  • 33 | {separatorSymbol} 34 |
  • 35 | ); 36 | }; 37 | 38 | const SeparatorItem = componentWithForwardedRef( 39 | SeparatorItemBase, 40 | "Breadcrumb.SeparatorItem", 41 | ); 42 | 43 | export default SeparatorItem; 44 | -------------------------------------------------------------------------------- /lib/TreeView/contexts.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export type LevelContextValue = number; 4 | export type SizeContextValue = number; 5 | 6 | export type TreeViewContextValue = { 7 | activeElement: HTMLElement | null; 8 | isSelectable: boolean; 9 | isMultiSelect: boolean; 10 | setActiveElement: React.Dispatch>; 11 | isDescendantSelected: (descendant: string) => boolean; 12 | isDescendantExpanded: (descendant: string) => boolean; 13 | handleDescendantSelect: (descendant: string) => void; 14 | handleDescendantCollapse: (descendant: string) => void; 15 | handleDescendantExpand: (descendant: string) => void; 16 | handleDescendantExpandToggle: (descendant: string) => void; 17 | }; 18 | 19 | export const LevelContext = React.createContext(null); 20 | 21 | export const SizeContext = React.createContext(null); 22 | 23 | export const TreeViewContext = React.createContext( 24 | null, 25 | ); 26 | 27 | if (process.env.NODE_ENV !== "production") { 28 | LevelContext.displayName = "TreeView.LevelContext"; 29 | SizeContext.displayName = "TreeView.SizeContext"; 30 | TreeViewContext.displayName = "TreeView.Context"; 31 | } 32 | -------------------------------------------------------------------------------- /lib/utils/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns value wrapped to the inclusive range of `min` and `max`. 3 | */ 4 | export const wrap = (number: number, min: number, max: number): number => 5 | min + ((((number - min) % (max - min)) + (max - min)) % (max - min)); 6 | 7 | /** 8 | * Returns value clamped to the inclusive range of `min` and `max`. 9 | */ 10 | export const clamp = (number: number, min: number, max: number): number => 11 | Math.max(Math.min(number, max), min); 12 | 13 | /** 14 | * Linear interpolate on the scale given by `a` to `b`, using `t` as the point on that scale. 15 | */ 16 | export const lerp = (a: number, b: number, t: number) => a + t * (b - a); 17 | 18 | /** 19 | * Inverse Linar Interpolation, get the fraction between `a` and `b` on which `v` resides. 20 | */ 21 | export const inLerp = (a: number, b: number, v: number) => (v - a) / (b - a); 22 | 23 | /** 24 | * Remap values from one linear scale to another. 25 | * 26 | * `oMin` and `oMax` are the scale on which the original value resides, 27 | * `rMin` and `rMax` are the scale to which it should be mapped. 28 | */ 29 | export const remap = ( 30 | v: number, 31 | oMin: number, 32 | oMax: number, 33 | rMin: number, 34 | rMax: number, 35 | ) => lerp(rMin, rMax, inLerp(oMin, oMax, v)); 36 | -------------------------------------------------------------------------------- /scripts/ci/publish-package.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { exec } from "node:child_process"; 3 | import * as fs from "node:fs/promises"; 4 | import * as path from "node:path"; 5 | import { promisify } from "node:util"; 6 | import { prerelease, valid } from "semver"; 7 | 8 | const execCmd = promisify(exec); 9 | 10 | const packagePath = process.cwd(); 11 | 12 | const distPath = path.join(packagePath, "./dist"); 13 | 14 | void (async () => { 15 | const distPackagePath = path.join(distPath, "package.json"); 16 | 17 | const packageJSON = JSON.parse( 18 | await fs.readFile(distPackagePath, "utf-8"), 19 | ) as Record; 20 | 21 | if (!packageJSON.version) { 22 | console.error("No `version` property found."); 23 | process.exit(1); 24 | } 25 | 26 | const version = valid(packageJSON.version as string); 27 | 28 | if (!version) { 29 | console.error("The `version` property isn't valid."); 30 | process.exit(1); 31 | } 32 | 33 | const prereleaseComponents = prerelease(version); 34 | const channel = (prereleaseComponents?.[0] ?? "latest") as string; 35 | 36 | const { stderr, stdout } = await execCmd( 37 | `npm publish ./dist/ --tag ${channel}`, 38 | ); 39 | 40 | console.log({ stdout }); 41 | console.error({ stderr }); 42 | })(); 43 | -------------------------------------------------------------------------------- /lib/Select/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { type LabelInfo } from "../internals"; 3 | import type { RegisteredElementsKeys } from "./Select"; 4 | import type { ElementsRegistry } from "./utils"; 5 | 6 | type ContextValue = { 7 | isListOpen: boolean; 8 | disabled: boolean; 9 | readOnly: boolean; 10 | keepMounted: boolean; 11 | multiple: boolean; 12 | searchable: boolean; 13 | isAnyOptionSelected: boolean; 14 | activeDescendant: HTMLElement | null; 15 | selectedValues: string | string[]; 16 | labelInfo: LabelInfo; 17 | filteredEntities: null | string[]; 18 | elementsRegistry: ElementsRegistry; 19 | closeListAndMaintainFocus: () => void; 20 | setActiveDescendant: React.Dispatch>; 21 | setFilteredEntities: React.Dispatch>; 22 | openList: () => void; 23 | closeList: () => void; 24 | toggleList: () => void; 25 | clearOptions: () => void; 26 | handleOptionClick: (value: string) => void; 27 | handleOptionRemove: (value: string) => void; 28 | }; 29 | 30 | const Context = React.createContext(null); 31 | 32 | if (process.env.NODE_ENV !== "production") { 33 | Context.displayName = "Select"; 34 | } 35 | 36 | export { Context as SelectContext, type ContextValue as SelectContextValue }; 37 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | workflow_call: 11 | 12 | jobs: 13 | test: 14 | name: Test components 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: pnpm/action-setup@v3 21 | with: 22 | version: 9 23 | 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: "pnpm" 28 | 29 | - name: "📦 install dependencies" 30 | run: pnpm install 31 | 32 | - name: "🔍 run tests" 33 | run: pnpm test 34 | 35 | lint: 36 | name: Code standards 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | steps: 40 | - name: "☁️ checkout repository" 41 | uses: actions/checkout@v4 42 | 43 | - name: "🔧 setup pnpm" 44 | uses: pnpm/action-setup@v3 45 | with: 46 | version: 9 47 | 48 | - name: "🔧 setup node" 49 | uses: actions/setup-node@v4 50 | with: 51 | node-version: 20 52 | cache: "pnpm" 53 | 54 | - name: "📦 install dependencies" 55 | run: pnpm install 56 | 57 | - name: "🔍 lint code" 58 | run: pnpm lint 59 | -------------------------------------------------------------------------------- /tests/utils/itSupportsStyle.tsx: -------------------------------------------------------------------------------- 1 | // Cherry picked from https://github.com/mantinedev/mantine/blob/master/src/mantine-tests/src/it-supports-style.tsx 2 | 3 | import * as React from "react"; 4 | import { render } from "."; 5 | 6 | const itSupportsStyle = ( 7 | Component: React.ComponentType, 8 | requiredProps: T, 9 | selector?: string, 10 | options?: { withPortal?: boolean }, 11 | ): void => { 12 | it("supports style prop", () => { 13 | const { withPortal = false } = options ?? {}; 14 | 15 | const getTarget = (container: HTMLElement): HTMLElement => { 16 | const portal = withPortal 17 | ? document.querySelector("[data-slot='Portal:Root']") 18 | : null; 19 | 20 | return selector 21 | ? portal 22 | ? (portal.querySelector(selector) as HTMLElement) 23 | : (container.querySelector(selector) as HTMLElement) 24 | : portal 25 | ? (container.firstChild as HTMLElement) 26 | : (container.firstChild as HTMLElement); 27 | }; 28 | 29 | const style = { border: "1px solid red", backgroundColor: "black" }; 30 | 31 | const { container } = render( 32 | , 36 | ); 37 | 38 | expect(getTarget(container)).toHaveStyle(style); 39 | }); 40 | }; 41 | 42 | export default itSupportsStyle; 43 | -------------------------------------------------------------------------------- /lib/Portal/Portal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { usePortalConfig } from "../PortalConfigProvider"; 4 | import { useIsomorphicValue } from "../utils"; 5 | 6 | export type Props = { 7 | /** 8 | * A function that will resolve the container element for the portal. 9 | * If not provided will opt-in `PortalConfigProvider` configuration as default behavior. 10 | * 11 | * Please note that this function is only called on the client-side. 12 | */ 13 | resolveContainer?: () => HTMLElement | null; 14 | /** 15 | * The children to render into the container. 16 | */ 17 | children: React.ReactNode; 18 | /** 19 | * If `true`, the `children` will be under the DOM hierarchy of the parent component. 20 | * 21 | * @default false 22 | */ 23 | disabled?: boolean; 24 | }; 25 | 26 | const Portal = (props: Props) => { 27 | const { resolveContainer, children, disabled = false } = props; 28 | 29 | const portalConfig = usePortalConfig(); 30 | 31 | const containerResolver = 32 | resolveContainer ?? portalConfig?.resolveContainer ?? (() => document.body); 33 | 34 | const container = useIsomorphicValue(containerResolver, null); 35 | 36 | if (disabled) return <>{children}; 37 | if (!container) return null; 38 | 39 | return ReactDOM.createPortal(children, container); 40 | }; 41 | 42 | export default Portal; 43 | -------------------------------------------------------------------------------- /tests/utils/itSupportsDataSetProps.tsx: -------------------------------------------------------------------------------- 1 | // Cherry picked from https://github.com/mantinedev/mantine/blob/master/src/mantine-tests/src/it-supports-others.tsx 2 | 3 | import * as React from "react"; 4 | import { render } from "."; 5 | 6 | const itSupportsDataSetProps = ( 7 | Component: React.ComponentType, 8 | requiredProps: T, 9 | selector?: string, 10 | options?: { withPortal?: boolean }, 11 | ): void => { 12 | it("supports `data-*` props", () => { 13 | const { withPortal = false } = options ?? {}; 14 | 15 | const getTarget = (container: HTMLElement): HTMLElement => { 16 | const portal = withPortal 17 | ? document.querySelector("[data-slot='Portal:Root']") 18 | : null; 19 | 20 | return selector 21 | ? portal 22 | ? (portal.querySelector(selector) as HTMLElement) 23 | : (container.querySelector(selector) as HTMLElement) 24 | : portal 25 | ? (container.firstChild as HTMLElement) 26 | : (container.firstChild as HTMLElement); 27 | }; 28 | 29 | const { container } = render( 30 | , 34 | ); 35 | 36 | expect(getTarget(container)).toHaveAttribute( 37 | "data-other-attribute", 38 | "test", 39 | ); 40 | }); 41 | }; 42 | 43 | export default itSupportsDataSetProps; 44 | -------------------------------------------------------------------------------- /lib/As/AsClone.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SystemError } from "../internals"; 3 | import type { UnknownObject } from "../types"; 4 | import { componentWithForwardedRef, forkRefs, isFragment } from "../utils"; 5 | import { type Props } from "./As"; 6 | import { mergeProps } from "./utils"; 7 | 8 | const AsCloneBase = ( 9 | props: Props & React.RefAttributes, 10 | ref: React.Ref, 11 | ) => { 12 | const { children, ...otherProps } = props; 13 | 14 | if (React.isValidElement(children)) { 15 | type SingleElement = typeof children; 16 | 17 | if (isFragment(children)) { 18 | throw new SystemError( 19 | "The component is not expected to receive a React Fragment child.", 20 | "As", 21 | ); 22 | } 23 | 24 | const childProps = (children as SingleElement).props as UnknownObject; 25 | const cloneProps = mergeProps(otherProps, childProps); 26 | 27 | cloneProps.ref = forkRefs( 28 | ref, 29 | (children as SingleElement & { ref: React.Ref }).ref, 30 | ); 31 | 32 | return React.cloneElement(children, cloneProps); 33 | } 34 | 35 | try { 36 | return React.Children.only(null); 37 | } catch { 38 | throw new SystemError( 39 | "The component expected to receive a single React element child.", 40 | "As", 41 | ); 42 | } 43 | }; 44 | 45 | const AsClone = componentWithForwardedRef(AsCloneBase, "AsClone"); 46 | 47 | export default AsClone; 48 | -------------------------------------------------------------------------------- /tests/utils/itIsPolymorphic.tsx: -------------------------------------------------------------------------------- 1 | // Cherry picked from https://github.com/mantinedev/mantine/blob/master/src/mantine-tests/src/it-is-polymorphic.tsx 2 | 3 | import * as React from "react"; 4 | import { render } from "."; 5 | 6 | const itIsPolymorphic = ( 7 | Component: React.ComponentType, 8 | requiredProps: T, 9 | selector?: string, 10 | ) => { 11 | it("is polymorphic", () => { 12 | const getTarget = (container: HTMLElement): HTMLElement => 13 | selector 14 | ? (container.querySelector(selector) as HTMLElement) 15 | : (container.firstChild as HTMLElement); 16 | 17 | const TestComponent = React.forwardRef( 18 | (props: Record = {}, ref: React.Ref) => ( 19 | 24 | ), 25 | ); 26 | 27 | TestComponent.displayName = "@styleless-ui/TestComponent"; 28 | 29 | const { container: withTag } = render( 30 | , 35 | ); 36 | 37 | const { container: withComponent } = render( 38 | , 42 | ); 43 | 44 | expect(getTarget(withTag).tagName).toBe("A"); 45 | expect(getTarget(withComponent).tagName).toBe("SPAN"); 46 | }); 47 | }; 48 | 49 | export default itIsPolymorphic; 50 | -------------------------------------------------------------------------------- /lib/Select/components/List/utils.ts: -------------------------------------------------------------------------------- 1 | import { PopperUtils } from "../../../utils"; 2 | 3 | const calcBoundaryOverflow = ( 4 | anchorElement: HTMLElement, 5 | element: HTMLElement, 6 | ) => { 7 | const elements = { anchorElement, popperElement: element }; 8 | const strategy: (typeof PopperUtils.strategies)[0] = "fixed"; 9 | 10 | const rects = PopperUtils.getElementRects(elements, strategy); 11 | 12 | const topSideCoordinates = { 13 | x: 0, 14 | y: rects.anchorRect.y - rects.popperRect.height, 15 | }; 16 | 17 | const bottomSideCoordinates = { 18 | x: 0, 19 | y: rects.anchorRect.y + rects.anchorRect.height, 20 | }; 21 | 22 | const overflowArgs = { strategy, elements, elementRects: rects }; 23 | 24 | const topSideOverflow = PopperUtils.detectBoundaryOverflow({ 25 | ...overflowArgs, 26 | coordinates: topSideCoordinates, 27 | }); 28 | 29 | const bottomSideOverflow = PopperUtils.detectBoundaryOverflow({ 30 | ...overflowArgs, 31 | coordinates: bottomSideCoordinates, 32 | }); 33 | 34 | return { 35 | topSideOverflow: topSideOverflow.top, 36 | bottomSideOverflow: bottomSideOverflow.bottom, 37 | }; 38 | }; 39 | 40 | export const calcSidePlacement = ( 41 | anchorElement: HTMLElement, 42 | element: HTMLElement, 43 | ) => { 44 | const { topSideOverflow, bottomSideOverflow } = calcBoundaryOverflow( 45 | anchorElement, 46 | element, 47 | ); 48 | 49 | if (topSideOverflow < bottomSideOverflow) return "top"; 50 | 51 | return "bottom"; 52 | }; 53 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - uses: pnpm/action-setup@v3 14 | with: 15 | version: 9 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: "pnpm" 21 | 22 | - name: "📦 install dependencies" 23 | run: pnpm install 24 | 25 | - name: "🧱 build package" 26 | run: pnpm build 27 | 28 | - name: "🗄️ archive package" 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: dist 32 | path: dist 33 | 34 | publish-npm: 35 | needs: build 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - uses: pnpm/action-setup@v3 41 | with: 42 | version: 9 43 | 44 | - uses: actions/setup-node@v4 45 | with: 46 | node-version: 20 47 | cache: "pnpm" 48 | registry-url: https://registry.npmjs.org/ 49 | 50 | - name: "📦 install dependencies" 51 | run: pnpm install 52 | 53 | - name: "🚚 download package" 54 | uses: actions/download-artifact@v4 55 | with: 56 | name: dist 57 | path: dist 58 | 59 | - name: "🚀 publish package" 60 | run: npx tsx ./scripts/ci/publish-package.ts 61 | env: 62 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 63 | -------------------------------------------------------------------------------- /lib/Dialog/components/Backdrop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { logger } from "../../internals"; 3 | import type { MergeElementProps } from "../../types"; 4 | import { componentWithForwardedRef } from "../../utils"; 5 | import { DialogContext } from "../context"; 6 | import { BackdropRoot as BackdropRootSlot } from "../slots"; 7 | 8 | type OwnProps = { 9 | /** 10 | * The className applied to the component. 11 | */ 12 | className?: string; 13 | }; 14 | 15 | export type Props = Omit< 16 | MergeElementProps<"div", OwnProps>, 17 | "defaultChecked" | "defaultValue" | "children" 18 | >; 19 | 20 | const BackdropBase = (props: Props, ref: React.Ref) => { 21 | const { className, onClick, ...otherProps } = props; 22 | 23 | const ctx = React.useContext(DialogContext); 24 | 25 | if (!ctx) { 26 | logger("You have to use this component as a descendant of .", { 27 | scope: "Dialog.Backdrop", 28 | type: "error", 29 | }); 30 | 31 | return null; 32 | } 33 | 34 | const handleClick = (event: React.MouseEvent) => { 35 | onClick?.(event); 36 | 37 | if (event.isDefaultPrevented()) return; 38 | 39 | ctx.emitClose(); 40 | }; 41 | 42 | return ( 43 |