From 3ee361dc2c28fd149131eb536d30cd5bc315792b Mon Sep 17 00:00:00 2001 From: Abdullajon1881 Date: Mon, 25 May 2026 16:13:50 +0500 Subject: [PATCH 1/6] chore: gitignore generated codeql databases and untrack codeql-db-js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The codeql-db-js/ directory is a local CodeQL CLI artifact (database snapshot produced by `codeql database create`). It is not source and should not be in version control — it bloats clones and re-creating it is the standard local workflow. - Add codeql-db-js/ and codeql-db-*/ to .gitignore - Untrack the currently checked-in copy --- .gitignore | 4 + codeql-db-js/baseline-info.json | 1 - codeql-db-js/codeql-database.yml | 46 --- ...-diagnostics-add-20260523T112944.961Z.json | 2 - ...-diagnostics-add-20260523T120803.711Z.json | 1 - .../extractors/javascript/autobuilder-1.jsonl | 1 - .../database-create-20260523.162940.875.log | 377 ------------------ 7 files changed, 4 insertions(+), 428 deletions(-) delete mode 100644 codeql-db-js/baseline-info.json delete mode 100644 codeql-db-js/codeql-database.yml delete mode 100644 codeql-db-js/diagnostic/cli-diagnostics-add-20260523T112944.961Z.json delete mode 100644 codeql-db-js/diagnostic/cli-diagnostics-add-20260523T120803.711Z.json delete mode 100644 codeql-db-js/diagnostic/extractors/javascript/autobuilder-1.jsonl delete mode 100644 codeql-db-js/log/database-create-20260523.162940.875.log diff --git a/.gitignore b/.gitignore index b14870f..1bfe250 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,7 @@ android/generated # React Native Nitro Modules nitrogen/ + +# CodeQL local databases (generated by the CodeQL CLI; not source) +codeql-db-js/ +codeql-db-*/ diff --git a/codeql-db-js/baseline-info.json b/codeql-db-js/baseline-info.json deleted file mode 100644 index bda1905..0000000 --- a/codeql-db-js/baseline-info.json +++ /dev/null @@ -1 +0,0 @@ -{"languages":{"javascript":{"displayName":"JavaScript/TypeScript","files":["src/DrumPicker.tsx","src/DateDrumPicker.tsx","src/DrumPicker.native.tsx","src/drumPickerLayout.ts","src/dateDrumPickerLogic.ts","src/DrumPickerViewNativeComponent.ts","src/index.tsx","src/types.ts","src/__tests__/dateDrumPickerLogic.test.ts","example/src/App.tsx","babel.config.js","eslint.config.mjs","react-native.config.js","example/babel.config.js","example/index.js","example/jest.config.js","example/metro.config.js","example/react-native.config.js"],"linesOfCode":934,"name":"javascript"},"java":{"displayName":"Java/Kotlin","files":["android/src/main/java/com/drumpicker/DrumPickerAdapter.kt","android/src/main/java/com/drumpicker/DrumPickerChangeEvent.kt","android/src/main/java/com/drumpicker/DrumPickerDefaults.kt","android/src/main/java/com/drumpicker/DrumPickerViewManager.kt","android/src/main/java/com/drumpicker/DrumPickerView.kt","android/src/main/java/com/drumpicker/DrumPickerPackage.kt","example/android/app/src/main/java/com/drumpicker.example/MainActivity.kt","example/android/app/src/main/java/com/drumpicker.example/MainApplication.kt"],"linesOfCode":846,"name":"java"},"actions":{"displayName":"GitHub Actions","files":[".github/actions/setup/action.yml",".github/workflows/ci.yml",".github/workflows/codeql.yml"],"linesOfCode":261,"name":"actions"},"cpp":{"displayName":"C/C++","files":["ios/DrumPickerView.h"],"linesOfCode":9,"name":"cpp"},"swift":{"displayName":"Swift","files":["example/ios/DrumPickerExample/AppDelegate.swift"],"linesOfCode":39,"name":"swift"}}} \ No newline at end of file diff --git a/codeql-db-js/codeql-database.yml b/codeql-db-js/codeql-database.yml deleted file mode 100644 index 6c60787..0000000 --- a/codeql-db-js/codeql-database.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -sourceLocationPrefix: C:\Users\ymaru\prj\react-native-drum-picker -baselineLinesOfCode: 934 -unicodeNewlines: true -columnKind: utf16 -primaryLanguage: javascript -inProgress: - primaryLanguage: javascript - installedExtractors: - actions: - - C:/Tools/codeql/actions - cpp: - - C:/Tools/codeql/cpp - csharp: - - C:/Tools/codeql/csharp - csv: - - C:/Tools/codeql/csv - go: - - C:/Tools/codeql/go - html: - - C:/Tools/codeql/html - java: - - C:/Tools/codeql/java - javascript: - - C:/Tools/codeql/javascript - properties: - - C:/Tools/codeql/properties - python: - - C:/Tools/codeql/python - ruby: - - C:/Tools/codeql/ruby - rust: - - C:/Tools/codeql/rust - swift: - - C:/Tools/codeql/swift - xml: - - C:/Tools/codeql/xml - yaml: - - C:/Tools/codeql/yaml -creationMetadata: - sha: f251ab784de3a47fcf36cc27056d4773ab0c15a7 - cliVersion: 2.25.5 - creationTime: 2026-05-23T11:29:44.924846400Z -overlayBaseDatabase: false -overlayDatabase: false -finalised: false diff --git a/codeql-db-js/diagnostic/cli-diagnostics-add-20260523T112944.961Z.json b/codeql-db-js/diagnostic/cli-diagnostics-add-20260523T112944.961Z.json deleted file mode 100644 index 4171046..0000000 --- a/codeql-db-js/diagnostic/cli-diagnostics-add-20260523T112944.961Z.json +++ /dev/null @@ -1,2 +0,0 @@ -{"timestamp":"2026-05-23T16:29:41.5255385+05:00","source":{"id":"cli/file-coverage-baseline","name":"File coverage baseline telemetry"},"visibility":{"cliSummaryTable":false,"statusPage":false,"telemetry":true},"attributes":{"durationMilliseconds":205}} -{"timestamp":"2026-05-23T16:29:44.9117671+05:00","source":{"id":"cli/platform","name":"Platform"},"visibility":{"cliSummaryTable":false,"statusPage":false,"telemetry":true},"attributes":{"name":"Windows 11","arch":"amd64","version":"10.0"}} diff --git a/codeql-db-js/diagnostic/cli-diagnostics-add-20260523T120803.711Z.json b/codeql-db-js/diagnostic/cli-diagnostics-add-20260523T120803.711Z.json deleted file mode 100644 index d3c28a0..0000000 --- a/codeql-db-js/diagnostic/cli-diagnostics-add-20260523T120803.711Z.json +++ /dev/null @@ -1 +0,0 @@ -{"timestamp":"2026-05-23T17:08:03.7110866+05:00","source":{"id":"cli/database/create","name":"CodeQL CLI: database create"},"plaintextMessage":"A fatal error occurred: Exit status 1 from command: [C:\\Tools\\codeql\\tools\\win64\\runner.exe, cmd.exe, /C, type, NUL, &&, C:\\Tools\\codeql\\javascript\\tools\\autobuild.cmd]","severity":"error","visibility":{"telemetry":false},"attributes":{"exitCode":2}} diff --git a/codeql-db-js/diagnostic/extractors/javascript/autobuilder-1.jsonl b/codeql-db-js/diagnostic/extractors/javascript/autobuilder-1.jsonl deleted file mode 100644 index d3a3997..0000000 --- a/codeql-db-js/diagnostic/extractors/javascript/autobuilder-1.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"timestamp":"2026-05-23T17:08:03.6019927+05:00","source":{"id":"js/internal-error","name":"Internal error","extractorName":"javascript"},"markdownMessage":"Internal error: java.nio.file.FileSystemException: C:\\Users\\ymaru\\prj\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker\\example\\node_modules\\react-native-drum-picker: Имя этого файла не может быть разрешено системой","severity":"unknown","visibility":{"cliSummaryTable":true,"statusPage":false,"telemetry":true}} diff --git a/codeql-db-js/log/database-create-20260523.162940.875.log b/codeql-db-js/log/database-create-20260523.162940.875.log deleted file mode 100644 index 4d93bb7..0000000 --- a/codeql-db-js/log/database-create-20260523.162940.875.log +++ /dev/null @@ -1,377 +0,0 @@ -[2026-05-23 16:29:40] This is codeql database create .\codeql-db-js --language=javascript-typescript --source-root=. -[2026-05-23 16:29:40] Log file was started late. -[2026-05-23 16:29:40] [PROGRESS] database create> Initializing database at C:\Users\ymaru\prj\react-native-drum-picker\codeql-db-js. -[2026-05-23 16:29:40] Running plumbing command: codeql database init --language=javascript-typescript --extractor-options-verbosity=1 --qlconfig-file=C:\Users\ymaru\prj\react-native-drum-picker\qlconfig.yml --source-root=C:\Users\ymaru\prj\react-native-drum-picker --allow-missing-source-root=false --allow-already-existing -- C:\Users\ymaru\prj\react-native-drum-picker\codeql-db-js -[2026-05-23 16:29:40] Calling plumbing command: codeql resolve languages --extractor-options-verbosity=1 --format=betterjson -[2026-05-23 16:29:41] [DETAILS] resolve languages> Scanning for [codeql-extractor.yml] from C:\Tools\codeql\.codeqlmanifest.json -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\actions\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\cpp\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\csharp\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\csv\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\go\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\html\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\java\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\javascript\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\properties\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\python\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\ruby\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\rust\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\swift\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\xml\codeql-extractor.yml. -[2026-05-23 16:29:41] [DETAILS] resolve languages> Parsing C:\Tools\codeql\yaml\codeql-extractor.yml. -[2026-05-23 16:29:41] Plumbing command codeql resolve languages completed: - { - "aliases" : { - "c" : "cpp", - "c++" : "cpp", - "c-c++" : "cpp", - "c-cpp" : "cpp", - "c#" : "csharp", - "java-kotlin" : "java", - "kotlin" : "java", - "javascript-typescript" : "javascript", - "typescript" : "javascript" - }, - "extractors" : { - "actions" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\actions", - "extractor_options" : { } - } - ], - "cpp" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\cpp", - "extractor_options" : { - "scale_timeouts" : { - "title" : "Value to scale compiler introspection timeouts with", - "description" : "The extractor attempts to determine what compiler the source code being extracted is compiled with. To this end the extractor makes additional calls to the compiler, some of which are expected to return within a certain fixed time (either 10s or 15s). On some systems that are under high load this time might be too short, and can be scaled up using this option.\n", - "type" : "string", - "pattern" : "[0-9]+" - }, - "log_verbosity" : { - "title" : "Verbosity of the extractor logging", - "description" : "Set the verbosity of the extractor logging to 'quiet' (0), 'normal' (1), 'chatty' (2), or 'noisy' (3). The default is 'normal'.\n", - "type" : "string", - "pattern" : "[0-3]" - } - } - } - ], - "csharp" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\csharp", - "extractor_options" : { - "trap" : { - "title" : "Options pertaining to TRAP.", - "description" : "Options pertaining to TRAP.", - "type" : "object", - "properties" : { - "compression" : { - "title" : "Controls compression for the TRAP files written by the extractor.", - "description" : "This option is only intended for use in debugging the extractor. Accepted values are 'brotli' (the default, to write brotli-compressed TRAP), 'gzip', and 'none' (to write uncompressed TRAP).\n", - "type" : "string", - "pattern" : "^(none|gzip|brotli)$" - } - } - }, - "buildless" : { - "title" : "DEPRECATED - Whether to use buildless (standalone) extraction.", - "description" : "DEPRECATED: Use `--build-mode none` instead.\nA value indicating, which type of extraction the autobuilder should perform. If 'true', then the standalone extractor will be used, otherwise tracing extraction will be performed. The default is 'false'. Note that buildless extraction will generally yield less accurate analysis results, and should only be used in cases where it is not possible to build the code (for example if it uses inaccessible dependencies).\n", - "type" : "string", - "pattern" : "^(false|true)$" - }, - "logging" : { - "title" : "Options pertaining to logging.", - "description" : "Options pertaining to logging.", - "type" : "object", - "properties" : { - "verbosity" : { - "title" : "Extractor logging verbosity level.", - "description" : "Controls the level of verbosity of the extractor. The supported levels are (in order of increasing verbosity):\n - off\n - errors\n - warnings\n - info or progress\n - debug or progress+\n - trace or progress++\n - progress+++\n", - "type" : "string", - "pattern" : "^(off|errors|warnings|(info|progress)|(debug|progress\\+)|(trace|progress\\+\\+)|progress\\+\\+\\+)$" - } - } - }, - "binlog" : { - "title" : "Binlog", - "description" : "[EXPERIMENTAL] The value is a path to the MsBuild binary log file that should be extracted. This option only works when `--build-mode none` is also specified.\n", - "type" : "array" - }, - "buildless_dependency_dir" : { - "title" : "The path where buildless (standalone) extraction should keep dependencies.", - "description" : "If set, the buildless (standalone) extractor will store dependencies in this directory.\n", - "type" : "string" - } - } - } - ], - "csv" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\csv" - } - ], - "go" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\go", - "extractor_options" : { - "extract_tests" : { - "title" : "Whether to include Go test files in the CodeQL database.", - "description" : "A value indicating whether Go test files should be included in the CodeQL database. The default is 'false'.\n", - "type" : "string", - "pattern" : "^(false|true)$" - }, - "extract_vendor_dirs" : { - "title" : "Whether to include Go vendor directories in the CodeQL database.", - "description" : "A value indicating whether Go vendor directories should be included in the CodeQL database. The default is 'false'.\n", - "type" : "string", - "pattern" : "^(false|true)$" - } - } - } - ], - "html" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\html" - } - ], - "java" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\java", - "extractor_options" : { - "exclude" : { - "title" : "A glob excluding files from analysis.", - "description" : "A glob indicating what files to exclude from the analysis. This accepts glob patterns that are supported by Java's 'getPathMatcher' implementation.\n", - "type" : "string" - }, - "add_prefer_source" : { - "title" : "Whether to always prefer source files over class files.", - "description" : "A value indicating whether source files should be preferred over class files. If set to 'true', the extraction adds '-Xprefer:source' to the javac command line. If set to 'false', the extraction uses the default javac behavior ('-Xprefer:newer'). The default is 'true'.\n", - "type" : "string", - "pattern" : "^(false|true)$" - }, - "buildless" : { - "title" : "Whether to use buildless (standalone) extraction (experimental).", - "description" : "A value indicating, which type of extraction the autobuilder should perform. If 'true', then the standalone extractor will be used, otherwise tracing extraction will be performed. The default is 'false'. Note that buildless extraction will generally yield less accurate analysis results, and should only be used in cases where it is not possible to build the code (for example if it uses inaccessible dependencies).\n", - "type" : "string", - "pattern" : "^(false|true)$" - }, - "buildless_dependency_dir" : { - "title" : "The path where buildless (standalone) extraction should keep dependencies.", - "description" : "If set, the buildless (standalone) extractor will store dependencies in this directory.\n", - "type" : "string" - }, - "minimize_dependency_jars" : { - "title" : "Whether to rewrite and minimize downloaded JAR dependencies (experimental).", - "description" : "If 'true', JAR dependencies downloaded during extraction will be rewritten to remove unneeded data, such as method bodies. The default is 'false'.\n", - "type" : "string", - "pattern" : "^(false|true)$" - } - } - } - ], - "javascript" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\javascript", - "extractor_options" : { - "skip_types" : { - "title" : "Skip type extraction for TypeScript", - "description" : "Whether to skip the extraction of types in a TypeScript application", - "type" : "string", - "pattern" : "^(false|true)$" - } - } - } - ], - "properties" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\properties" - } - ], - "python" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\python", - "extractor_options" : { - "logging" : { - "title" : "Options pertaining to logging.", - "description" : "Options pertaining to logging.", - "type" : "object", - "properties" : { - "verbosity" : { - "title" : "Python extractor logging verbosity level.", - "description" : "Controls the level of verbosity of the CodeQL Python extractor.\nThe supported levels are (in order of increasing verbosity):\n\n - off\n - errors\n - warnings\n - info or progress\n - debug or progress+\n - trace or progress++\n - progress+++\n", - "type" : "string", - "pattern" : "^(off|errors|warnings|(info|progress)|(debug|progress\\+)|(trace|progress\\+\\+)|progress\\+\\+\\+)$" - } - } - }, - "python_executable_name" : { - "title" : "Controls the name of the Python executable used by the Python extractor.", - "description" : "The Python extractor uses platform-dependent heuristics to determine the name of the Python executable to use. Specifying a value for this option overrides the name of the Python executable used by the extractor. Accepted values are py, python and python3. Use this setting with caution, the Python extractor requires Python 3 to run.\n", - "type" : "string", - "pattern" : "^(py|python|python3)$" - } - } - } - ], - "ruby" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\ruby", - "extractor_options" : { - "trap" : { - "title" : "Options pertaining to TRAP.", - "description" : "Options pertaining to TRAP.", - "type" : "object", - "properties" : { - "compression" : { - "title" : "Controls compression for the TRAP files written by the extractor.", - "description" : "This option is only intended for use in debugging the extractor. Accepted values are 'gzip' (the default, to write gzip-compressed TRAP) 'zstd' (to write Zstandard-compressed TRAP) and 'none' (to write uncompressed TRAP).\n", - "type" : "string", - "pattern" : "^(none|gzip|zstd)$" - } - } - } - } - } - ], - "rust" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\rust", - "extractor_options" : { - "trap" : { - "title" : "Options pertaining to TRAP.", - "description" : "Options pertaining to TRAP.", - "type" : "object", - "properties" : { - "compression" : { - "title" : "Controls compression for the TRAP files written by the extractor.", - "description" : "This option is only intended for use in debugging the extractor. Accepted values are 'gzip' (to write gzip-compressed TRAP) 'zstd' (to write Zstandard-compressed TRAP) and 'none' (the default, to write uncompressed TRAP).\n", - "type" : "string", - "pattern" : "^(none|gzip|zstd)$" - } - } - }, - "cargo_target_dir" : { - "title" : "Directory to use for cargo output files.", - "description" : "This value is an optional path to use as `CARGO_TARGET_DIR` for the internal cargo commands the extractor uses. Pointing it to a persistent directory may reduce execution time of consecutive extractor runs. By default, a new scratch directory is used for each run.\n", - "type" : "string" - }, - "cargo_target" : { - "title" : "Target architecture", - "description" : "Target architecture to use for analysis, analogous to `cargo --target`. By default the host architecture is used.\n", - "type" : "string" - }, - "cargo_features" : { - "title" : "Cargo features to turn on", - "description" : "Comma-separated list of features to turn on. By default all features are enabled. If any features are specified, then only those features are enabled. The `default` feature must be explicitly specified if only default features are desired. Can be repeated.\n", - "type" : "array" - }, - "cargo_cfg_overrides" : { - "title" : "Cargo cfg overrides", - "description" : "Comma-separated list of cfg settings to enable, or disable if prefixed with `-`. Can be repeated.\n", - "type" : "array" - }, - "logging" : { - "title" : "Options pertaining to logging.", - "description" : "Options pertaining to logging.", - "type" : "object", - "properties" : { - "verbosity" : { - "title" : "Extractor logging verbosity level.", - "description" : "Controls the level of verbosity of the extractor. The supported levels are (in order of increasing verbosity):\n - off\n - errors\n - warnings\n - info or progress\n - debug or progress+\n - trace or progress++\n - progress+++\n", - "type" : "string", - "pattern" : "^(off|errors|warnings|(info|progress)|(debug|progress\\+)|(trace|progress\\+\\+)|progress\\+\\+\\+)$" - }, - "flamegraph" : { - "title" : "[Experimental] File path for write flame graph log", - "description" : "Collect flame graph data using the `tracing-flame` crate. To render a flame graph or chart, run the `inferno-flamegraph` command. See also: https://crates.io/crates/tracing-flame\n", - "type" : "string" - } - } - }, - "extract_dependencies_as_source" : { - "title" : "Extract dependencies as source code", - "description" : "Extract the full source code of dependencies instead of only extracting signatures.\n", - "type" : "string", - "pattern" : "^(false|true)$" - } - } - } - ], - "swift" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\swift" - } - ], - "xml" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\xml" - } - ], - "yaml" : [ - { - "extractor_root" : "C:\\Tools\\codeql\\yaml" - } - ] - } - } -[2026-05-23 16:29:41] [PROGRESS] database init> Calculating baseline information in C:\Users\ymaru\prj\react-native-drum-picker -[2026-05-23 16:29:41] [SPAMMY] database init> Ignoring the following directories when processing baseline information: .git, .hg, .svn. -[2026-05-23 16:29:41] [DETAILS] database init> Running command in C:\Users\ymaru\prj\react-native-drum-picker: C:\Tools\codeql\tools\win64\scc.exe --by-file --exclude-dir .git,.hg,.svn --format json --no-large --no-min . -[2026-05-23 16:29:41] Using configure-baseline script C:\Tools\codeql\javascript\tools\configure-baseline.cmd. -[2026-05-23 16:29:41] [PROGRESS] database init> Running command in C:\Users\ymaru\prj\react-native-drum-picker: [C:\Tools\codeql\javascript\tools\configure-baseline.cmd] -[2026-05-23 16:29:41] Using configure-baseline script C:\Tools\codeql\actions\tools\configure-baseline.cmd. -[2026-05-23 16:29:41] [PROGRESS] database init> Running command in C:\Users\ymaru\prj\react-native-drum-picker: [C:\Tools\codeql\actions\tools\configure-baseline.cmd] -[2026-05-23 16:29:41] [SPAMMY] database init> Ignored an additional 3 files when processing baseline information for YAML due to paths and paths-ignore configuration. -[2026-05-23 16:29:41] [SPAMMY] database init> Found 18 baseline files for javascript. -[2026-05-23 16:29:41] [SPAMMY] database init> Found 8 baseline files for java. -[2026-05-23 16:29:41] [SPAMMY] database init> Found 3 baseline files for actions. -[2026-05-23 16:29:41] [SPAMMY] database init> Found 1 baseline files for cpp. -[2026-05-23 16:29:41] [SPAMMY] database init> Found 1 baseline files for swift. -[2026-05-23 16:29:41] [PROGRESS] database init> Calculated baseline information for languages: actions, cpp, java, javascript, swift (205ms). -[2026-05-23 16:29:44] [PROGRESS] database init> Resolving extractor javascript. -[2026-05-23 16:29:44] [DETAILS] database init> Found candidate extractor root for javascript: C:\Tools\codeql\javascript. -[2026-05-23 16:29:44] [PROGRESS] database init> Successfully loaded extractor JavaScript/TypeScript (javascript) from C:\Tools\codeql\javascript. -[2026-05-23 16:29:44] [PROGRESS] database init> Created skeleton CodeQL database at C:\Users\ymaru\prj\react-native-drum-picker\codeql-db-js. This in-progress database is ready to be populated by an extractor. -[2026-05-23 16:29:44] Plumbing command codeql database init completed. -[2026-05-23 16:29:44] [PROGRESS] database create> Running build command: [] -[2026-05-23 16:29:45] Running plumbing command: codeql database trace-command --working-dir=C:\Users\ymaru\prj\react-native-drum-picker --index-traceless-dbs --no-db-cluster -- C:\Users\ymaru\prj\react-native-drum-picker\codeql-db-js -[2026-05-23 16:29:45] Using autobuild script C:\Tools\codeql\javascript\tools\autobuild.cmd. -[2026-05-23 16:29:45] [PROGRESS] database trace-command> Running command in C:\Users\ymaru\prj\react-native-drum-picker: [C:\Tools\codeql\javascript\tools\autobuild.cmd] -[2026-05-23 16:29:45] [build-stdout] Single-threaded extraction. -[2026-05-23 17:08:03] [build-stderr] java.nio.file.FileSystemException: C:\Users\ymaru\prj\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker\example\node_modules\react-native-drum-picker: ��� ����� ����� �� ����� ���� ��������� �������� -[2026-05-23 17:08:03] [build-stderr] at java.base/sun.nio.fs.WindowsException.translateToIOException(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/sun.nio.fs.WindowsDirectoryStream.(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/sun.nio.fs.WindowsFileSystemProvider.newDirectoryStream(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/java.nio.file.Files.newDirectoryStream(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/java.nio.file.FileTreeWalker.visit(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/java.nio.file.FileTreeWalker.next(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/java.nio.file.Files.walkFileTree(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at java.base/java.nio.file.Files.walkFileTree(Unknown Source) -[2026-05-23 17:08:03] [build-stderr] at com.semmle.js.extractor.AutoBuild.findFilesToExtract(AutoBuild.java:1201) -[2026-05-23 17:08:03] [build-stderr] at com.semmle.js.extractor.AutoBuild.extractSource(AutoBuild.java:755) -[2026-05-23 17:08:03] [build-stderr] at com.semmle.js.extractor.AutoBuild.run(AutoBuild.java:468) -[2026-05-23 17:08:03] [build-stderr] at com.semmle.js.extractor.AutoBuild.main(AutoBuild.java:1387) -[2026-05-23 17:08:03] [ERROR] Spawned process exited abnormally (code 1; tried to run: [C:\Tools\codeql\tools\win64\runner.exe, cmd.exe, /C, type, NUL, &&, C:\Tools\codeql\javascript\tools\autobuild.cmd]) -[2026-05-23 17:08:03] Exception caught at top level: Exit status 1 from command: [C:\Tools\codeql\tools\win64\runner.exe, cmd.exe, /C, type, NUL, &&, C:\Tools\codeql\javascript\tools\autobuild.cmd] - com.semmle.cli2.Commandline.executeAndCheckResult(Commandline.java:170) - com.semmle.cli2.Commandline.runWithoutReturn(Commandline.java:123) - com.semmle.cli2.database.DatabaseProcessCommandCommon.executeSubcommand(DatabaseProcessCommandCommon.java:227) - com.semmle.cli2.database.TraceCommandCommand.executeSubcommand(TraceCommandCommand.java:126) - com.semmle.cli2.picocli.SubcommandCommon.lambda$executeSubcommandWithMessages$5(SubcommandCommon.java:886) - com.semmle.cli2.picocli.SubcommandCommon.withCompilationMessages(SubcommandCommon.java:444) - com.semmle.cli2.picocli.SubcommandCommon.executeSubcommandWithMessages(SubcommandCommon.java:884) - com.semmle.cli2.picocli.PlumbingRunner.run(PlumbingRunner.java:123) - com.semmle.cli2.picocli.SubcommandCommon.runPlumbingInProcess(SubcommandCommon.java:201) - com.semmle.cli2.database.CreateCommand.executeSubcommand(CreateCommand.java:188) - com.semmle.cli2.picocli.SubcommandCommon.lambda$executeSubcommandWithMessages$5(SubcommandCommon.java:886) - com.semmle.cli2.picocli.SubcommandCommon.withCompilationMessages(SubcommandCommon.java:444) - com.semmle.cli2.picocli.SubcommandCommon.executeSubcommandWithMessages(SubcommandCommon.java:884) - com.semmle.cli2.picocli.SubcommandCommon.toplevelMain(SubcommandCommon.java:777) - com.semmle.cli2.picocli.SubcommandCommon.call(SubcommandCommon.java:757) - com.semmle.cli2.picocli.SubcommandMaker.runMain(SubcommandMaker.java:238) - com.semmle.cli2.picocli.SubcommandMaker.runMain(SubcommandMaker.java:259) - com.semmle.cli2.CodeQL.main(CodeQL.java:113) From 68f439dd5ce948755fffaeeef30d89cd4c3f6ffb Mon Sep 17 00:00:00 2001 From: Abdullajon1881 Date: Mon, 25 May 2026 16:17:01 +0500 Subject: [PATCH 2/6] feat: support labeled {label, value} items in DrumPicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DrumPicker now accepts either string items (existing behavior) or labeled items of the form { label, value } where `value` can be any typed payload (string, number, enum, object). The label is what the wheel displays; the resolved `value` is surfaced on `onChange` as `nativeEvent.item`, fully typed via the new generic parameter on DrumPicker. Why: callers currently have to maintain a parallel lookup array to map the selected index/label back to a domain id. This is the single most common ergonomics ask for picker components and removes a class of off-by-one bugs when the items list changes. Implementation notes: - The native Fabric spec still receives string[] only. The JS wrapper extracts labels via getItemLabel() before handing to native and looks up the resolved value via getItemValue() when the native onValueChange fires. - Fully back-compatible: existing string[] callers see no change at runtime. The native event payload gains an `item` field; for plain strings, `item === value`, so any code reading either keeps working. - Date/Time wrappers are unchanged — they already pass strings. Tests: 6 new behavior tests cover string back-compat, labeled items, typed object values, mixed arrays, and out-of-bounds index fallback. Full suite: 42/42 green. --- README.md | 42 ++++++- src/DrumPicker.native.tsx | 48 ++++++-- src/DrumPicker.tsx | 2 +- .../DrumPicker.labeledItems.test.tsx | 110 ++++++++++++++++++ src/__tests__/DrumPicker.test.tsx | 1 + src/index.tsx | 7 +- src/types.ts | 48 +++++++- 7 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/DrumPicker.labeledItems.test.tsx diff --git a/README.md b/README.md index 576ea33..1397054 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,46 @@ const [index, setIndex] = useState(1); /> ``` +### Labeled items: display one thing, receive another + +When you want to render human-readable text but receive a typed identifier +(an enum value, database id, country code, etc.) on selection, pass +`{ label, value }` items instead of strings: + +```tsx +type CountryCode = 'us' | 'de' | 'jp'; + +const COUNTRIES: Array<{ label: string; value: CountryCode }> = [ + { label: 'United States', value: 'us' }, + { label: 'Germany', value: 'de' }, + { label: 'Japan', value: 'jp' }, +]; + + + items={COUNTRIES} + onChange={(event) => { + // event.nativeEvent.value === 'United States' (the label that was shown) + // event.nativeEvent.item === 'us' (the typed value, fully inferred) + setCountry(event.nativeEvent.item); + }} +/> +``` + +Plain string items keep working exactly as before — for them, `item` simply +equals `value`, so `event.nativeEvent.item` is always safe to read. + +`value` can be any type — primitives, ids, or full objects: + +```tsx + console.log(e.nativeEvent.item.iso)} +/> +``` + ### `onChange` and expensive side effects Native emits `onChange` when the wheel **snaps to idle** and the **centered index changes** (duplicate indices are ignored). Use it for UI state. For AsyncStorage, APIs, or analytics, **debounce** in your app: @@ -220,7 +260,7 @@ Day count follows month/year (e.g. February has 28/29 days). | Prop | Type | Default | Description | |------|------|---------|-------------| -| `items` | `string[]` | required | Wheel labels | +| `items` | `Array` | required | Wheel rows. Strings are used as both label and value; labeled items render `label` and report `value` back on `onChange` | | `selectedIndex` | `number` | `0` | Selected row index | | `itemHeight` | `number` | `44` | Row height (dp) | | `visibleItemCount` | `number` | `5` | Visible rows (odd recommended) | diff --git a/src/DrumPicker.native.tsx b/src/DrumPicker.native.tsx index ad9ff97..3ef23fe 100644 --- a/src/DrumPicker.native.tsx +++ b/src/DrumPicker.native.tsx @@ -1,8 +1,14 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import type { NativeSyntheticEvent } from 'react-native'; import DrumPickerNative from './DrumPickerViewNativeComponent'; +import type { DrumPickerChangeEventPayload } from './DrumPickerViewNativeComponent'; import { resolveDrumPickerStyle } from './drumPickerLayout'; -import type { DrumPickerChangeEvent, DrumPickerProps } from './types'; +import { + getItemLabel, + getItemValue, + type DrumPickerChangeEvent, + type DrumPickerProps, +} from './types'; const DEFAULTS = { selectedIndex: 0, @@ -21,7 +27,7 @@ const DEFAULTS = { hapticFeedback: false, } as const; -export function DrumPicker({ +export function DrumPicker({ items, selectedIndex = DEFAULTS.selectedIndex, itemHeight = DEFAULTS.itemHeight, @@ -40,26 +46,54 @@ export function DrumPicker({ onChange, style, testID, -}: DrumPickerProps) { +}: DrumPickerProps) { const pickerStyle = resolveDrumPickerStyle( itemHeight, visibleItemCount, style ); + // Native only understands strings — extract labels once per items change. + const labels = useMemo(() => items.map(getItemLabel), [items]); + + // Keep the latest items array in a ref so onValueChange can look up the + // resolved value without re-subscribing every render. + const itemsRef = useRef(items); + useEffect(() => { + itemsRef.current = items; + }, [items]); + const lastEmittedIndexRef = useRef(selectedIndex); useEffect(() => { lastEmittedIndexRef.current = selectedIndex; }, [selectedIndex]); const handleValueChange = useCallback( - (event: NativeSyntheticEvent) => { + (event: NativeSyntheticEvent) => { const index = event.nativeEvent.index; if (index === lastEmittedIndexRef.current) { return; } lastEmittedIndexRef.current = index; - onChange?.(event); + if (!onChange) { + return; + } + const currentItems = itemsRef.current; + const sourceItem = + index >= 0 && index < currentItems.length + ? currentItems[index] + : undefined; + const enriched: NativeSyntheticEvent> = { + ...event, + nativeEvent: { + ...event.nativeEvent, + item: + sourceItem !== undefined + ? getItemValue(sourceItem) + : (event.nativeEvent.value as unknown as T), + }, + }; + onChange(enriched); }, [onChange] ); @@ -68,7 +102,7 @@ export function DrumPicker({ (_props: DrumPickerProps): never { throw new Error( "'react-native-drum-picker' is only supported on native platforms." ); diff --git a/src/__tests__/DrumPicker.labeledItems.test.tsx b/src/__tests__/DrumPicker.labeledItems.test.tsx new file mode 100644 index 0000000..a67da6a --- /dev/null +++ b/src/__tests__/DrumPicker.labeledItems.test.tsx @@ -0,0 +1,110 @@ +import { render } from '@testing-library/react-native'; +import React from 'react'; +import { + fireNativeDrumPickerChange, + getLatestNativeDrumPickerProps, + resetNativeDrumPickerMocks, +} from '../__mocks__/DrumPickerViewNativeComponent'; +import { DrumPicker } from '../DrumPicker.native'; + +describe('DrumPicker — labeled items', () => { + beforeEach(() => { + jest.clearAllMocks(); + resetNativeDrumPickerMocks(); + }); + + it('passes only string labels to the native component when given labeled items', () => { + render( + + ); + expect(getLatestNativeDrumPickerProps()?.items).toEqual([ + 'Small', + 'Medium', + 'Large', + ]); + }); + + it('surfaces the resolved item on onChange.nativeEvent.item', () => { + const onChange = jest.fn(); + render( + + ); + fireNativeDrumPickerChange(2, 'Large'); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].nativeEvent).toEqual({ + index: 2, + value: 'Large', + item: 'l', + }); + }); + + it('supports non-string item values (numbers, objects)', () => { + type Country = { id: number; iso: string }; + const items: Array<{ label: string; value: Country }> = [ + { label: 'United States', value: { id: 1, iso: 'us' } }, + { label: 'Germany', value: { id: 2, iso: 'de' } }, + ]; + const onChange = jest.fn(); + render( items={items} onChange={onChange} />); + fireNativeDrumPickerChange(1, 'Germany'); + expect(onChange.mock.calls[0][0].nativeEvent.item).toEqual({ + id: 2, + iso: 'de', + }); + }); + + it('still works with plain string items (back-compat)', () => { + const onChange = jest.fn(); + render( + + ); + fireNativeDrumPickerChange(1, 'Beta'); + expect(onChange.mock.calls[0][0].nativeEvent).toEqual({ + index: 1, + value: 'Beta', + item: 'Beta', + }); + }); + + it('handles mixed string + labeled items', () => { + const onChange = jest.fn(); + render( + + ); + expect(getLatestNativeDrumPickerProps()?.items).toEqual(['Plain', 'Fancy']); + fireNativeDrumPickerChange(1, 'Fancy'); + expect(onChange.mock.calls[0][0].nativeEvent).toEqual({ + index: 1, + value: 'Fancy', + item: 'fancy-id', + }); + }); + + it('falls back to value when the native index is out of bounds', () => { + const onChange = jest.fn(); + render( + + ); + fireNativeDrumPickerChange(5, 'Stale'); + expect(onChange.mock.calls[0][0].nativeEvent.item).toBe('Stale'); + }); +}); diff --git a/src/__tests__/DrumPicker.test.tsx b/src/__tests__/DrumPicker.test.tsx index 3d7a826..b2e6c37 100644 --- a/src/__tests__/DrumPicker.test.tsx +++ b/src/__tests__/DrumPicker.test.tsx @@ -39,6 +39,7 @@ describe('DrumPicker', () => { expect(onChange.mock.calls[0][0].nativeEvent).toEqual({ index: 1, value: 'Beta', + item: 'Beta', }); }); diff --git a/src/index.tsx b/src/index.tsx index a6302ca..8131f5c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,12 @@ export { getDaysInMonth, normalizeYearRange, } from './DateDrumPicker'; -export type { DrumPickerChangeEvent, DrumPickerProps } from './types'; +export type { + DrumPickerChangeEvent, + DrumPickerItem, + DrumPickerLabeledItem, + DrumPickerProps, +} from './types'; export type { DateDrumPickerColumnKey, DateDrumPickerMode, diff --git a/src/types.ts b/src/types.ts index c5b9da6..e67d9f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,35 @@ import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native'; -export type DrumPickerChangeEvent = { +/** + * A labeled picker item — the wheel renders `label`, and the resolved + * `value` is surfaced back on `onChange.nativeEvent.item`. + * + * Use this shape when you want the picker to display human-readable text + * but receive a typed identifier (id, enum value, etc.) on selection, + * without maintaining a parallel lookup array. + */ +export type DrumPickerLabeledItem = { + readonly label: string; + readonly value: T; +}; + +/** A picker item — either a plain string or a `{ label, value }` pair. */ +export type DrumPickerItem = string | DrumPickerLabeledItem; + +export type DrumPickerChangeEvent = { index: number; + /** The label shown for the selected row. */ value: string; + /** + * The resolved item value at `index`. When labeled items are supplied, + * this is the `value` field; for plain string items, this equals `value` + * (so reading `item` is always safe). + */ + item: T; }; -export type DrumPickerProps = { - items: string[]; +export type DrumPickerProps = { + items: ReadonlyArray>; selectedIndex?: number; itemHeight?: number; visibleItemCount?: number; @@ -21,7 +44,24 @@ export type DrumPickerProps = { itemBackgroundColor?: string; containerBackgroundColor?: string; hapticFeedback?: boolean; - onChange?: (event: NativeSyntheticEvent) => void; + onChange?: (event: NativeSyntheticEvent>) => void; style?: StyleProp; testID?: string; }; + +/** + * Internal helper: extract the label for a picker item. Exported because the + * Date/Time wrappers also need to normalize items before passing them down. + */ +export function getItemLabel(item: DrumPickerItem): string { + return typeof item === 'string' ? item : item.label; +} + +/** + * Internal helper: extract the resolved value for a picker item. For plain + * strings this returns the string itself (typed as `T` since the consumer + * chose `T = string` in that case). + */ +export function getItemValue(item: DrumPickerItem): T { + return (typeof item === 'string' ? item : item.value) as T; +} From 310b908743175107f8ccb8d726605cae23e40159 Mon Sep 17 00:00:00 2001 From: Abdullajon1881 Date: Mon, 25 May 2026 16:21:16 +0500 Subject: [PATCH 3/6] feat: add web fallback for DrumPicker using native element: - SSR-safe: no throw at module evaluation - Keyboard-navigable and screen-reader-friendly by default (browser semantics) - Honors the same value/onChange contract as native — callers read event.nativeEvent.index and event.nativeEvent.value identically - Mirrors native's "no duplicate-index re-emit" behavior so controlled state loops are avoided Props that translate to web (items, selectedIndex, textColor, selectedTextColor, textSize, backgroundColor, visibleItemCount as ` element, accessible, no drum animation) | Requires **React Native 0.76+** with the **New Architecture** enabled. @@ -84,6 +84,23 @@ Requires **React Native 0.76+** with the **New Architecture** enabled. This package is an **Android Fabric View** library. Use a **development build** or `expo run:android` after `expo prebuild` — not Expo Go. +### Web fallback + +On web (Expo Web, `react-native-web`, or SSR contexts), `DrumPicker` +renders a native HTML `` element so that: + * + * - SSR / `react-native-web` builds don't crash at module load + * - The picker is keyboard-navigable and screen-reader-friendly by default + * - The same `value` and `onChange` contract works cross-platform — callers + * read `event.nativeEvent.index`, `value`, and `item` the same way + * + * Labeled `{ label, value }` items are supported here too: the wheel renders + * `label`, and `onChange` reports the resolved `value` on `nativeEvent.item`. + * + * A drum-style scroll wheel on web is a separate, larger feature; this + * fallback gives the lib a useful baseline web experience today. + */ +export function DrumPicker({ + items, + selectedIndex = DEFAULTS.selectedIndex, + itemHeight = DEFAULTS.itemHeight, + visibleItemCount = DEFAULTS.visibleItemCount, + textColor = DEFAULTS.textColor, + selectedTextColor = DEFAULTS.selectedTextColor, + textSize = DEFAULTS.textSize, + backgroundColor = DEFAULTS.backgroundColor, + onChange, + style, + testID, +}: DrumPickerProps) { + const safeIndex = Math.min( + Math.max(selectedIndex, 0), + Math.max(items.length - 1, 0) + ); + + // Mirror native's "don't re-emit identical index" contract so React state + // bouncing back into the controlled value doesn't loop. + const lastEmittedIndexRef = useRef(safeIndex); + useEffect(() => { + lastEmittedIndexRef.current = safeIndex; + }, [safeIndex]); + + const flatStyle = useMemo( + () => + (StyleSheet.flatten(style as StyleProp) ?? {}) as ViewStyle, + [style] + ); + + const inlineStyle = useMemo(() => { + const heightFromStyle = + typeof flatStyle.height === 'number' ? flatStyle.height : undefined; + const widthFromStyle = + typeof flatStyle.width === 'number' ? flatStyle.width : undefined; + return { + width: widthFromStyle ?? '100%', + height: heightFromStyle ?? itemHeight * Math.max(visibleItemCount, 1), + color: selectedTextColor, + background: backgroundColor, + fontSize: textSize, + border: 'none', + outline: 'none', + WebkitAppearance: 'none', + MozAppearance: 'none', + appearance: 'none', + textAlign: 'center', + textAlignLast: 'center', + }; + }, [ + backgroundColor, + flatStyle.height, + flatStyle.width, + itemHeight, + selectedTextColor, + textSize, + visibleItemCount, + ]); + + const optionStyle = useMemo( + () => ({ + color: textColor, + background: backgroundColor, + }), + [backgroundColor, textColor] + ); + + const handleChange = useCallback( + (event: WebSelectChangeEvent) => { + const index = event.target.selectedIndex; + if (index === lastEmittedIndexRef.current) { + return; + } + lastEmittedIndexRef.current = index; + if (!onChange) { + return; + } + const source = items[index]; + const label = + source !== undefined ? getItemLabel(source) : ''; + const item: T = + source !== undefined + ? getItemValue(source) + : (label as unknown as T); + // Synthesize a payload shaped like the native event so calling code + // does not need to branch on platform. + const synthetic = { + nativeEvent: { index, value: label, item }, + target: event.target, + currentTarget: event.currentTarget, + bubbles: event.bubbles, + cancelable: event.cancelable, + defaultPrevented: event.defaultPrevented, + eventPhase: event.eventPhase, + isTrusted: event.isTrusted, + timeStamp: event.timeStamp, + type: 'change', + preventDefault: () => event.preventDefault(), + stopPropagation: () => event.stopPropagation(), + isDefaultPrevented: () => event.defaultPrevented, + isPropagationStopped: () => false, + persist: () => {}, + }; + onChange( + synthetic as unknown as Parameters>[0] + ); + }, + [items, onChange] + ); + + // Use createElement so we don't depend on react-native-web's createElement + // override and we keep this file framework-light. + return createElement( + 'select', + { + 'value': safeIndex, + 'onChange': handleChange, + 'style': inlineStyle, + 'data-testid': testID, + 'aria-label': 'Picker', + 'size': Math.max(visibleItemCount, 1), + }, + items.map((item, index) => + createElement( + 'option', + { key: index, value: index, style: optionStyle }, + getItemLabel(item) + ) + ) ); } diff --git a/src/__tests__/DrumPickerWeb.test.tsx b/src/__tests__/DrumPickerWeb.test.tsx new file mode 100644 index 0000000..9b5d01a --- /dev/null +++ b/src/__tests__/DrumPickerWeb.test.tsx @@ -0,0 +1,162 @@ +/** + * Web-fallback tests for `DrumPicker`. + * + * Jest's `react-native` preset resolves `'../DrumPicker'` to the + * `.native.tsx` variant. We import the `.tsx` file explicitly so this test + * exercises the web fallback module without changing project-wide resolver + * config. + */ +import * as React from 'react'; +import TestRenderer, { act } from 'react-test-renderer'; +import { DrumPicker as WebDrumPicker } from '../DrumPicker.tsx'; + +type SelectInstance = { + type: string; + props: { + 'value': number; + 'onChange': (e: { + target: { selectedIndex: number }; + preventDefault?: () => void; + stopPropagation?: () => void; + }) => void; + 'style': React.CSSProperties; + 'data-testid'?: string; + 'aria-label'?: string; + 'size': number; + 'children': Array<{ type: string; props: { children: string } }>; + }; +}; + +function renderWeb(props: Parameters[0]): SelectInstance { + let renderer: TestRenderer.ReactTestRenderer; + act(() => { + renderer = TestRenderer.create(React.createElement(WebDrumPicker, props)); + }); + const root = renderer!.toTree(); + // The root rendered element is the , got: ${ + rendered ? rendered.type : JSON.stringify(root) + }` + ); + } + return rendered; +} + +describe('DrumPicker — web fallback', () => { + it('does not throw at import time (SSR safety)', () => { + expect(typeof WebDrumPicker).toBe('function'); + }); + + it('renders a . DateDrumPicker and TimeDrumPicker give each column a distinct default label (Day/Month/Year, Hour/Minute/Second/AM-PM) via columnAccessibilityLabels. Maintainer (scrollDynasty): - Example app: added "Labeled" (typed-value DrumPicker) and "TimePicker" (TimeDrumPicker) demo tabs so the new APIs are verifiable on device. - Extracted shared DRUM_PICKER_DEFAULTS so web and native cannot drift. - Fixed misleading getItemLabel comment in types.ts. - README: documented labeled-items scope (DrumPicker only), the TimeDrumPicker controlled clamp-and-notify behavior, and added an Accessibility section. Verification: typecheck, build, and 110 jest tests across 11 suites all pass locally (added accessibility + tie-break coverage). eslint clean on src (the only local failures are CRLF from the Windows working copy; the committed blobs are LF). --- example/src/App.tsx | 59 ++++++++++++- src/DateDrumPicker.tsx | 22 +++++ src/DrumPicker.native.tsx | 20 +---- src/DrumPicker.tsx | 20 +---- src/TimeDrumPicker.tsx | 29 +++++++ src/__tests__/DrumPickerWeb.test.tsx | 7 +- src/__tests__/accessibility.test.tsx | 86 +++++++++++++++++++ .../DateDrumPicker.snapshot.test.tsx.snap | 3 + src/__tests__/timeDrumPickerLogic.test.ts | 13 +++ src/drumPickerDefaults.ts | 25 ++++++ src/timeDrumPickerLogic.ts | 10 ++- src/types.ts | 38 ++++++-- 12 files changed, 286 insertions(+), 46 deletions(-) create mode 100644 src/__tests__/accessibility.test.tsx create mode 100644 src/drumPickerDefaults.ts diff --git a/example/src/App.tsx b/example/src/App.tsx index 80bb4c9..b67df6c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,9 +3,20 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { DateDrumPicker, DrumPicker, + TimeDrumPicker, type DateDrumPickerValue, + type TimeDrumPickerValue, } from 'react-native-drum-picker'; +type CountryCode = 'us' | 'de' | 'jp' | 'uz'; + +const COUNTRIES: Array<{ label: string; value: CountryCode }> = [ + { label: 'United States', value: 'us' }, + { label: 'Germany', value: 'de' }, + { label: 'Japan', value: 'jp' }, + { label: 'Uzbekistan', value: 'uz' }, +]; + const ITEM_HEIGHT = 44; const VISIBLE_COUNT = 5; const PICKER_HEIGHT = ITEM_HEIGHT * VISIBLE_COUNT; @@ -40,7 +51,14 @@ function useDebouncedCallback void>( export default function App() { const [example, setExample] = useState< - 'basic' | 'time' | 'hw' | 'date' | 'controlled' | 'debounce' + | 'basic' + | 'labeled' + | 'timepicker' + | 'time' + | 'hw' + | 'date' + | 'controlled' + | 'debounce' >('basic'); const [sizeIndex, setSizeIndex] = useState(1); @@ -49,6 +67,11 @@ export default function App() { const [heightIndex, setHeightIndex] = useState(75); const [weightIndex, setWeightIndex] = useState(90); const [controlledIndex, setControlledIndex] = useState(2); + const [country, setCountry] = useState('uz'); + const [time, setTime] = useState({ + hour: 9, + minute: 30, + }); const [debouncedLog, setDebouncedLog] = useState('—'); const [date, setDate] = useState({ day: 10, @@ -64,6 +87,8 @@ export default function App() { () => [ ['basic', 'Basic'], + ['labeled', 'Labeled'], + ['timepicker', 'TimePicker'], ['time', 'Time'], ['hw', 'Height / weight'], ['date', 'Date'], @@ -108,6 +133,38 @@ export default function App() { )} + {example === 'labeled' && ( + + + Labeled items (label shown, value returned) + + + items={COUNTRIES} + selectedIndex={COUNTRIES.findIndex((c) => c.value === country)} + onChange={(e) => setCountry(e.nativeEvent.item)} + accessibilityLabel="Country" + style={styles.pickerW120} + /> + Selected code: {country} + + )} + + {example === 'timepicker' && ( + + TimeDrumPicker (12h, 15-min steps) + + + {String(time.hour ?? 0).padStart(2, '0')}: + {String(time.minute ?? 0).padStart(2, '0')} + + + )} + {example === 'time' && ( Time (hour · minute) diff --git a/src/DateDrumPicker.tsx b/src/DateDrumPicker.tsx index 7a96501..81cb98d 100644 --- a/src/DateDrumPicker.tsx +++ b/src/DateDrumPicker.tsx @@ -65,10 +65,24 @@ export type DateDrumPickerProps = { columnStyle?: StyleProp; columnStyles?: Partial>>; columnTestIDs?: Partial>; + /** + * Accessibility labels per column. Defaults to `Day` / `Month` / `Year` so + * each wheel is distinguishable to assistive tech instead of all reading + * "Picker". + */ + columnAccessibilityLabels?: Partial< + Record + >; }; type DateColumnKey = DateDrumPickerColumnKey; +const DEFAULT_COLUMN_ACCESSIBILITY_LABELS: Record = { + day: 'Day', + month: 'Month', + year: 'Year', +}; + const COLUMN_ORDER: Record = { 'day': ['day'], 'month': ['month'], @@ -114,6 +128,7 @@ export function DateDrumPicker({ columnStyle, columnStyles, columnTestIDs, + columnAccessibilityLabels, }: DateDrumPickerProps) { const currentYear = new Date().getFullYear(); const { minYear, maxYear } = useMemo( @@ -211,6 +226,10 @@ export function DateDrumPicker({ columnStyles?.[column], ]; + const columnAccessibilityLabel = (column: DateColumnKey): string => + columnAccessibilityLabels?.[column] ?? + DEFAULT_COLUMN_ACCESSIBILITY_LABELS[column]; + const renderColumn = (column: DateColumnKey) => { if (column === 'day') { return ( @@ -218,6 +237,7 @@ export function DateDrumPicker({ key="day" {...sharedPickerProps} testID={columnTestIDs?.day} + accessibilityLabel={columnAccessibilityLabel('day')} style={columnContainerStyle('day')} items={dayItems} selectedIndex={Math.min(resolvedValue.day - 1, dayItems.length - 1)} @@ -239,6 +259,7 @@ export function DateDrumPicker({ key="month" {...sharedPickerProps} testID={columnTestIDs?.month} + accessibilityLabel={columnAccessibilityLabel('month')} style={columnContainerStyle('month')} items={monthItems} selectedIndex={resolvedValue.month - 1} @@ -259,6 +280,7 @@ export function DateDrumPicker({ key="year" {...sharedPickerProps} testID={columnTestIDs?.year} + accessibilityLabel={columnAccessibilityLabel('year')} style={columnContainerStyle('year')} items={yearItems} selectedIndex={resolvedValue.year - minYear} diff --git a/src/DrumPicker.native.tsx b/src/DrumPicker.native.tsx index 3ef23fe..e038c98 100644 --- a/src/DrumPicker.native.tsx +++ b/src/DrumPicker.native.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import type { NativeSyntheticEvent } from 'react-native'; import DrumPickerNative from './DrumPickerViewNativeComponent'; import type { DrumPickerChangeEventPayload } from './DrumPickerViewNativeComponent'; +import { DRUM_PICKER_DEFAULTS as DEFAULTS } from './drumPickerDefaults'; import { resolveDrumPickerStyle } from './drumPickerLayout'; import { getItemLabel, @@ -10,23 +11,6 @@ import { type DrumPickerProps, } from './types'; -const DEFAULTS = { - selectedIndex: 0, - itemHeight: 44, - visibleItemCount: 5, - textColor: '#8E8E93', - selectedTextColor: '#1C1C1E', - textSize: 20, - selectedTextSize: 22, - showSelectionIndicator: true, - selectionIndicatorColor: '#D1D1D6', - selectionIndicatorHeight: 1, - backgroundColor: 'transparent', - itemBackgroundColor: 'transparent', - containerBackgroundColor: 'transparent', - hapticFeedback: false, -} as const; - export function DrumPicker({ items, selectedIndex = DEFAULTS.selectedIndex, @@ -43,6 +27,7 @@ export function DrumPicker({ itemBackgroundColor = DEFAULTS.itemBackgroundColor, containerBackgroundColor = DEFAULTS.containerBackgroundColor, hapticFeedback = DEFAULTS.hapticFeedback, + accessibilityLabel, onChange, style, testID, @@ -101,6 +86,7 @@ export function DrumPicker({ return ( void; }; -const DEFAULTS = { - selectedIndex: 0, - itemHeight: 44, - visibleItemCount: 5, - textColor: '#8E8E93', - selectedTextColor: '#1C1C1E', - textSize: 20, - backgroundColor: 'transparent', -} as const; - /** * Web fallback for `DrumPicker`. * @@ -68,6 +55,7 @@ export function DrumPicker({ selectedTextColor = DEFAULTS.selectedTextColor, textSize = DEFAULTS.textSize, backgroundColor = DEFAULTS.backgroundColor, + accessibilityLabel = DEFAULTS.accessibilityLabel, onChange, style, testID, @@ -179,7 +167,7 @@ export function DrumPicker({ 'onChange': handleChange, 'style': inlineStyle, 'data-testid': testID, - 'aria-label': 'Picker', + 'aria-label': accessibilityLabel, 'size': Math.max(visibleItemCount, 1), }, items.map((item, index) => diff --git a/src/TimeDrumPicker.tsx b/src/TimeDrumPicker.tsx index 5197d0f..b999ff2 100644 --- a/src/TimeDrumPicker.tsx +++ b/src/TimeDrumPicker.tsx @@ -75,6 +75,24 @@ export type TimeDrumPickerProps = { columnStyle?: StyleProp; columnStyles?: Partial>>; columnTestIDs?: Partial>; + /** + * Accessibility labels per column. Defaults to `Hour` / `Minute` / + * `Second` / `AM/PM` so each wheel is distinguishable to assistive tech + * instead of all reading "Picker". + */ + columnAccessibilityLabels?: Partial< + Record + >; +}; + +const DEFAULT_COLUMN_ACCESSIBILITY_LABELS: Record< + TimeDrumPickerColumnKey, + string +> = { + hour: 'Hour', + minute: 'Minute', + second: 'Second', + period: 'AM/PM', }; const COLUMN_ORDER: Record = { @@ -129,6 +147,7 @@ export function TimeDrumPicker({ columnStyle, columnStyles, columnTestIDs, + columnAccessibilityLabels, }: TimeDrumPickerProps) { const resolvedHourFormat = hourFormat ?? defaultHourFormat(mode); const normalizedMinuteInterval = useMemo( @@ -262,11 +281,18 @@ export function TimeDrumPicker({ columnStyles?.[column], ]; + const columnAccessibilityLabel = ( + column: TimeDrumPickerColumnKey + ): string => + columnAccessibilityLabels?.[column] ?? + DEFAULT_COLUMN_ACCESSIBILITY_LABELS[column]; + const renderHour = () => ( { expect(select.props['data-testid']).toBe('my-picker'); }); - it('exposes an aria-label for screen readers', () => { + it('exposes a default aria-label for screen readers', () => { const select = renderWeb({ items: ['A'] }); expect(select.props['aria-label']).toBe('Picker'); }); + it('uses a custom accessibilityLabel as aria-label', () => { + const select = renderWeb({ items: ['A'], accessibilityLabel: 'Country' }); + expect(select.props['aria-label']).toBe('Country'); + }); + it('synthesizes a native-like event on selection change', () => { const onChange = jest.fn(); const select = renderWeb({ diff --git a/src/__tests__/accessibility.test.tsx b/src/__tests__/accessibility.test.tsx new file mode 100644 index 0000000..bf02c4f --- /dev/null +++ b/src/__tests__/accessibility.test.tsx @@ -0,0 +1,86 @@ +import { render, screen } from '@testing-library/react-native'; +import React from 'react'; +import { + getLatestNativeDrumPickerProps, + resetNativeDrumPickerMocks, +} from '../__mocks__/DrumPickerViewNativeComponent'; +import { DrumPicker } from '../DrumPicker.native'; +import { DateDrumPicker } from '../DateDrumPicker'; +import { TimeDrumPicker } from '../TimeDrumPicker'; + +function getNativePickers() { + return screen.getAllByTestId('drum-picker-native'); +} + +describe('accessibilityLabel forwarding', () => { + beforeEach(() => { + jest.clearAllMocks(); + resetNativeDrumPickerMocks(); + }); + + it('forwards accessibilityLabel to the native DrumPicker view', () => { + render(); + expect(getLatestNativeDrumPickerProps()?.accessibilityLabel).toBe('Size'); + }); + + it('omits accessibilityLabel when not provided (native picks its own)', () => { + render(); + expect( + getLatestNativeDrumPickerProps()?.accessibilityLabel + ).toBeUndefined(); + }); + + it('DateDrumPicker labels each column distinctly by default', () => { + render( + + ); + const labels = getNativePickers().map( + (p) => p.props.accessibilityLabel + ); + expect(labels).toEqual(['Day', 'Month', 'Year']); + }); + + it('DateDrumPicker honors custom columnAccessibilityLabels', () => { + render( + + ); + const labels = getNativePickers().map( + (p) => p.props.accessibilityLabel + ); + expect(labels).toEqual(['Día', 'Mes']); + }); + + it('TimeDrumPicker labels each column distinctly by default', () => { + render( + + ); + const labels = getNativePickers().map( + (p) => p.props.accessibilityLabel + ); + expect(labels).toEqual(['Hour', 'Minute', 'Second', 'AM/PM']); + }); + + it('TimeDrumPicker honors custom columnAccessibilityLabels', () => { + render( + + ); + const labels = getNativePickers().map( + (p) => p.props.accessibilityLabel + ); + expect(labels).toEqual(['Часы', 'Минуты']); + }); +}); diff --git a/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap b/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap index e726e94..62def80 100644 --- a/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap +++ b/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap @@ -14,6 +14,7 @@ exports[`DateDrumPicker snapshots matches day-month-year mode 1`] = ` } > { expect(snapToInterval(59, 30)).toBe(30); expect(snapToInterval(58, 20)).toBe(40); }); + it('rounds exact half-step ties down (matches docstring)', () => { + expect(snapToInterval(1, 2)).toBe(0); // 0.5 step -> down + expect(snapToInterval(3, 2)).toBe(2); // 1.5 step -> down + expect(snapToInterval(5, 10)).toBe(0); // 0.5 step -> down + expect(snapToInterval(15, 10)).toBe(10); // 1.5 step -> down + expect(snapToInterval(3, 6)).toBe(0); // 0.5 step -> down + }); + it('still rounds clearly-closer values to the nearer mark', () => { + expect(snapToInterval(8, 15)).toBe(15); + expect(snapToInterval(7, 15)).toBe(0); + expect(snapToInterval(26, 10)).toBe(30); + expect(snapToInterval(24, 10)).toBe(20); + }); }); describe('to12Hour / from12Hour', () => { diff --git a/src/drumPickerDefaults.ts b/src/drumPickerDefaults.ts new file mode 100644 index 0000000..641221a --- /dev/null +++ b/src/drumPickerDefaults.ts @@ -0,0 +1,25 @@ +/** + * Default prop values for `DrumPicker`, shared between the native + * (`DrumPicker.native.tsx`) and web (`DrumPicker.tsx`) implementations so the + * two platforms can never silently drift apart. + * + * These mirror the visual defaults baked into the native views + * (`DrumPickerDefaults` in Kotlin / Swift). Keep them in sync if those change. + */ +export const DRUM_PICKER_DEFAULTS = { + selectedIndex: 0, + itemHeight: 44, + visibleItemCount: 5, + textColor: '#8E8E93', + selectedTextColor: '#1C1C1E', + textSize: 20, + selectedTextSize: 22, + showSelectionIndicator: true, + selectionIndicatorColor: '#D1D1D6', + selectionIndicatorHeight: 1, + backgroundColor: 'transparent', + itemBackgroundColor: 'transparent', + containerBackgroundColor: 'transparent', + hapticFeedback: false, + accessibilityLabel: 'Picker', +} as const; diff --git a/src/timeDrumPickerLogic.ts b/src/timeDrumPickerLogic.ts index b9f0224..dc89833 100644 --- a/src/timeDrumPickerLogic.ts +++ b/src/timeDrumPickerLogic.ts @@ -67,8 +67,9 @@ export function clampSecond(second: number): number { /** * Snap a 0..59 value to the nearest multiple of `interval` that is still - * within range. Ties round down (deterministic and matches floor semantics - * users expect when the picker first lands on a value). + * within range. Exact half-step ties round **down** (deterministic and + * matches the floor semantics users expect when the picker first lands on a + * value, e.g. minute 30 with interval 60 stays on the lower mark). */ export function snapToInterval( value: number, @@ -78,7 +79,10 @@ export function snapToInterval( if (interval === 1) { return safe; } - const snapped = Math.round(safe / interval) * interval; + // Round half *down*: Math.round rounds halves up, so subtract 0.5 and + // take the ceiling. e.g. 3 / 2 -> ceil(1.0) = 1 -> 2 (not 4). + const steps = Math.ceil(safe / interval - 0.5); + const snapped = steps * interval; const max = Math.floor(59 / interval) * interval; return Math.min(max, Math.max(0, snapped)); } diff --git a/src/types.ts b/src/types.ts index e67d9f4..dbcc028 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,8 +13,20 @@ export type DrumPickerLabeledItem = { readonly value: T; }; -/** A picker item — either a plain string or a `{ label, value }` pair. */ -export type DrumPickerItem = string | DrumPickerLabeledItem; +/** + * A picker item. + * + * - When `T` is `string` (the default), an item may be either a plain string + * or a `{ label, value }` pair. + * - When `T` is a non-string type, items **must** be `{ label, value }` pairs + * so the resolved `value` on `onChange.nativeEvent.item` is genuinely `T`. + * + * The `[T] extends [string]` wrapping keeps the check non-distributive so + * union value types behave predictably. + */ +export type DrumPickerItem = [T] extends [string] + ? string | DrumPickerLabeledItem + : DrumPickerLabeledItem; export type DrumPickerChangeEvent = { index: number; @@ -44,24 +56,34 @@ export type DrumPickerProps = { itemBackgroundColor?: string; containerBackgroundColor?: string; hapticFeedback?: boolean; + /** + * Accessibility label for the whole picker. Forwarded to the native view's + * `accessibilityLabel` and, on web, to the `` element's +`aria-label` (defaults to `Picker`). + +`DateDrumPicker` and `TimeDrumPicker` render multiple columns and give each a +distinct default label (`Day` / `Month` / `Year`, +`Hour` / `Minute` / `Second` / `AM/PM`) so assistive technologies can tell the +wheels apart instead of announcing every column as "Picker". Override per +column with `columnAccessibilityLabels`: + +```tsx + +``` + ## Styling Backgrounds are **transparent by default**. Only text and optional indicator lines are visible.